"""Market API endpoints."""
from __future__ import annotations

import json
import logging
from datetime import datetime, timedelta
from flask import Blueprint, jsonify, request, Response, stream_with_context
from flask_login import login_required, current_user
from app.helpers.auth import admin_required, get_current_user_id
from sqlalchemy import func, or_
from app.extensions import db
logger = logging.getLogger(__name__)
from app.models.asset import Asset, AssetProfile
from app.models.asset_source import AssetSourceMapping
from app.models.ohlcv import OHLCVData
from app.models.watchlist import Watchlist
from app.models.settings import AppSettings
from app.helpers.asset_filter import apply_asset_filter

api_market_bp = Blueprint('api_market', __name__)

# Simple cache for icon stats (refreshed every 60s)
_icon_stats_cache = {'data': None, 'ts': 0}


def get_icon_stats_cached():
    """Get icon stats with simple time-based cache."""
    import time
    now = time.time()
    if _icon_stats_cache['data'] is None or now - _icon_stats_cache['ts'] > 60:
        from app.services.icon_downloader import get_icon_stats
        _icon_stats_cache['data'] = get_icon_stats()
        _icon_stats_cache['ts'] = now
    return _icon_stats_cache['data']


@api_market_bp.route('/assets')
@login_required
def get_coins():
    """Get all tracked assets."""
    assets = apply_asset_filter(Asset.query.filter_by(is_active=True))\
        .order_by(
            db.case((Asset.market_cap_rank.is_(None), 1), else_=0),
            Asset.market_cap_rank.asc()
        ).all()
    return jsonify([c.to_dict() for c in assets])


@api_market_bp.route('/assets/paginated')
@login_required
def get_coins_paginated():
    """Get assets with server-side pagination, search, and filtering.

    Query params:
        page (int): Page number, default 1
        per_page (int): Items per page, default 25 (options: 10,25,50,100)
        q (str): Search query (matches name, symbol, id)
        status (str): 'all', 'active', 'inactive'
        sort (str): Sort field - 'rank', 'name', 'symbol', 'id', 'created'
        order (str): 'asc' or 'desc'
        has_data (str): 'all', 'yes', 'no' - filter by whether asset has OHLCV data
    """
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 25, type=int)
    per_page = min(per_page, 200)  # Cap at 200
    q = request.args.get('q', '').strip()
    status = request.args.get('status', 'all')
    sort = request.args.get('sort', 'rank')
    order = request.args.get('order', 'asc')
    has_data = request.args.get('has_data', 'all')

    # Base query — filtered by current asset mode
    query = apply_asset_filter(Asset.query)

    # Status filter
    if status == 'active':
        query = query.filter(Asset.is_active.is_(True))
    elif status == 'inactive':
        query = query.filter(Asset.is_active.is_(False))

    # Search filter
    if q:
        search = f'%{q}%'
        query = query.filter(or_(
            Asset.name.ilike(search),
            Asset.symbol.ilike(search),
            Asset.id.ilike(search),
            Asset.blockchain.ilike(search),
        ))

    # has_data filter — subquery for assets that have OHLCV records
    if has_data == 'yes':
        coins_with_data = db.session.query(OHLCVData.asset_id).distinct().subquery()
        query = query.filter(Asset.id.in_(db.session.query(coins_with_data)))
    elif has_data == 'no':
        coins_with_data = db.session.query(OHLCVData.asset_id).distinct().subquery()
        query = query.filter(~Asset.id.in_(db.session.query(coins_with_data)))

    # Get total before pagination for stats
    total = query.count()

    # Count active/inactive within current search/filter (excluding status filter)
    stats_query = apply_asset_filter(Asset.query)
    if q:
        search = f'%{q}%'
        stats_query = stats_query.filter(or_(
            Asset.name.ilike(search),
            Asset.symbol.ilike(search),
            Asset.id.ilike(search),
            Asset.blockchain.ilike(search),
        ))
    total_all = stats_query.count()
    total_active = stats_query.filter(Asset.is_active.is_(True)).count()
    total_inactive = total_all - total_active

    # Sorting
    if sort == 'name':
        order_col = Asset.name.desc() if order == 'desc' else Asset.name.asc()
        query = query.order_by(order_col)
    elif sort == 'symbol':
        order_col = Asset.symbol.desc() if order == 'desc' else Asset.symbol.asc()
        query = query.order_by(order_col)
    elif sort == 'id':
        order_col = Asset.id.desc() if order == 'desc' else Asset.id.asc()
        query = query.order_by(order_col)
    elif sort == 'created':
        order_col = Asset.created_at.desc() if order == 'desc' else Asset.created_at.asc()
        query = query.order_by(order_col)
    else:  # 'rank' default
        if order == 'desc':
            query = query.order_by(
                db.case((Asset.market_cap_rank.is_(None), 0), else_=1),
                Asset.market_cap_rank.desc(),
            )
        else:
            query = query.order_by(
                db.case((Asset.market_cap_rank.is_(None), 1), else_=0),
                Asset.market_cap_rank.asc(),
                Asset.name.asc(),
            )

    # Paginate
    pagination = query.paginate(page=page, per_page=per_page, error_out=False)
    assets = pagination.items

    # Batch-load source mappings and data counts for this page
    asset_ids = [c.id for c in assets]

    # Source mappings per asset
    mappings_raw = AssetSourceMapping.query.filter(
        AssetSourceMapping.asset_id.in_(asset_ids)
    ).all() if asset_ids else []
    mappings_by_coin = {}
    for m in mappings_raw:
        mappings_by_coin.setdefault(m.asset_id, []).append({
            'source': m.source,
            'source_asset_id': m.source_asset_id,
            'is_available': m.is_available,
        })

    # OHLCV data counts per asset
    ohlcv_counts = {}
    if asset_ids:
        rows = db.session.query(
            OHLCVData.asset_id, func.count(OHLCVData.id)
        ).filter(OHLCVData.asset_id.in_(asset_ids)).group_by(OHLCVData.asset_id).all()
        ohlcv_counts = {r[0]: r[1] for r in rows}

    # Profile counts per asset
    profile_counts = {}
    if asset_ids:
        rows = db.session.query(
            AssetProfile.asset_id, func.count(AssetProfile.id)
        ).filter(AssetProfile.asset_id.in_(asset_ids)).group_by(AssetProfile.asset_id).all()
        profile_counts = {r[0]: r[1] for r in rows}

    # Check which assets have local icons
    from app.services.icon_downloader import has_local_icon, get_local_icon_url

    # Build result
    items = []
    for c in assets:
        local_icon = get_local_icon_url(c.id, c.asset_type) if has_local_icon(c.id, c.asset_type) else None
        items.append({
            **c.to_dict(),
            'local_icon_url': local_icon,
            'sources': mappings_by_coin.get(c.id, []),
            'ohlcv_count': ohlcv_counts.get(c.id, 0),
            'profile_count': profile_counts.get(c.id, 0),
        })

    return jsonify({
        'items': items,
        'page': page,
        'per_page': per_page,
        'total': total,
        'total_pages': pagination.pages,
        'has_next': pagination.has_next,
        'has_prev': pagination.has_prev,
        'stats': {
            'total_all': total_all,
            'total_active': total_active,
            'total_inactive': total_inactive,
            'total_mappings': AssetSourceMapping.query.count(),
            'icons': get_icon_stats_cached(),
        },
    })


@api_market_bp.route('/asset/<asset_id>')
@login_required
def get_coin(asset_id):
    """Get single asset with latest profile."""
    asset = Asset.query.get_or_404(asset_id)
    profile = AssetProfile.query.filter_by(asset_id=asset_id)\
        .order_by(AssetProfile.fetched_at.desc()).first()

    result = asset.to_dict()
    result['profile'] = profile.to_dict() if profile else None
    return jsonify(result)


@api_market_bp.route('/asset/<asset_id>/ohlcv')
@login_required
def get_ohlcv(asset_id):
    """Get OHLCV data for a asset."""
    timeframe = request.args.get('timeframe', '1h')
    from app.helpers.asset_filter import get_source_for_coin
    source = request.args.get('source') or get_source_for_coin(asset_id)
    limit = request.args.get('limit', 200, type=int)

    records = OHLCVData.query.filter_by(
        asset_id=asset_id, timeframe=timeframe, source=source
    ).order_by(OHLCVData.datetime_wib.desc()).limit(limit).all()

    # Reverse to chronological order
    records.reverse()
    return jsonify([r.to_dict() for r in records])


@api_market_bp.route('/sync', methods=['POST'])
@admin_required
def sync_data():
    """Sync market data for a asset."""
    data = request.get_json() or {}
    asset_id = data.get('asset_id')
    provider_id = data.get('provider')

    if not asset_id:
        return jsonify({'error': 'asset_id is required'}), 400

    from app.services.data_sync.sync_manager import SyncManager
    manager = SyncManager()
    # Ensure asset exists first
    manager.ensure_coin_exists(asset_id, data.get('symbol', ''), data.get('name', ''))
    result = manager.sync_coin(asset_id, provider_id)
    return jsonify(result)


@api_market_bp.route('/sync/ohlcv', methods=['POST'])
@admin_required
def sync_ohlcv():
    """Sync OHLCV data for a specific asset/timeframe."""
    data = request.get_json() or {}
    asset_id = data.get('asset_id')
    timeframe = data.get('timeframe', '1h')
    provider_id = data.get('provider')

    if not asset_id:
        return jsonify({'error': 'asset_id is required'}), 400

    from app.services.data_sync.sync_manager import SyncManager
    manager = SyncManager()
    count = manager.sync_ohlcv_only(asset_id, timeframe, provider_id)
    return jsonify({'asset_id': asset_id, 'timeframe': timeframe, 'records_upserted': count})


@api_market_bp.route('/search')
@login_required
def search_coins():
    """Search for assets — local DB first, external API only if requested."""
    q = request.args.get('q', '')
    source = request.args.get('source', 'local')  # 'local' or 'external'
    if len(q) < 1:
        return jsonify([])

    if source == 'external':
        from app.services.data_sync.router import DataRouter
        router = DataRouter()
        results = router.search_coins(q)
        return jsonify(results)

    # Fast local DB search
    # If asset_type is explicitly set, filter by that; otherwise use session mode
    explicit_asset_type = request.args.get('asset_type')
    base_query = Asset.query.filter(
        Asset.is_active == True,
        db.or_(
            Asset.symbol.ilike(f'%{q}%'),
            Asset.name.ilike(f'%{q}%'),
            Asset.id.ilike(f'%{q}%'),
        )
    )
    if explicit_asset_type == 'all':
        pass  # No filter — search across all asset types (used by portfolio)
    elif explicit_asset_type in ('crypto', 'stock', 'stock_us'):
        base_query = base_query.filter(Asset.asset_type == explicit_asset_type)
    else:
        base_query = apply_asset_filter(base_query)

    assets = base_query.order_by(
        db.case((Asset.market_cap_rank.is_(None), 1), else_=0),
        Asset.market_cap_rank.asc()
    ).limit(20).all()
    return jsonify([c.to_dict() for c in assets])


from app.models.watchlist_group import WatchlistGroup


@api_market_bp.route('/watchlist', methods=['GET'])
@login_required
def get_watchlist():
    """Get watchlist items, filtered by current asset_mode and optionally by group_id."""
    from app.helpers.asset_filter import get_asset_mode
    asset_mode = get_asset_mode()

    group_id = request.args.get('group_id', type=int)
    query = Watchlist.query.join(Asset, Watchlist.asset_id == Asset.id)\
        .filter(Watchlist.user_id == get_current_user_id(),
                Asset.asset_type == asset_mode)
    if group_id is not None:
        query = query.filter(Watchlist.group_id == group_id)
    items = query.order_by(Watchlist.display_order.asc()).all()
    return jsonify([w.to_dict() for w in items])


@api_market_bp.route('/watchlist', methods=['POST'])
@login_required
def add_to_watchlist():
    """Add asset to watchlist (optionally into a group).

    Returns enriched response with asset + profile data so the frontend
    can insert the item without a page reload.
    """
    data = request.get_json() or {}
    asset_id = data.get('asset_id')
    group_id = data.get('group_id')
    if not asset_id:
        return jsonify({'error': 'asset_id is required'}), 400

    # Check duplicate within same group
    existing = Watchlist.query.filter_by(asset_id=asset_id, group_id=group_id, user_id=get_current_user_id()).first()
    if existing:
        return jsonify({'message': 'Already in this watchlist group'}), 200

    max_order = db.session.query(db.func.max(Watchlist.display_order)) \
        .filter_by(group_id=group_id, user_id=get_current_user_id()).scalar() or 0
    item = Watchlist(asset_id=asset_id, group_id=group_id,
                     user_id=get_current_user_id(),
                     display_order=max_order + 1,
                     notes=data.get('notes', ''))
    db.session.add(item)
    try:
        db.session.commit()
    except Exception as exc:
        db.session.rollback()
        import logging
        logging.getLogger(__name__).warning('add_to_watchlist commit failed: %s', exc)
        return jsonify({'error': 'Gagal menambahkan ke watchlist',
                        'detail': str(exc)}), 409

    # Enrich response with asset + profile data for no-reload update
    asset = db.session.get(Asset, asset_id)
    profile = AssetProfile.query.filter_by(asset_id=asset_id)\
        .order_by(AssetProfile.fetched_at.desc()).first()

    result = {
        'watchlist': item.to_dict(),
        'asset': {
            'id': asset.id, 'name': asset.name,
            'symbol': asset.symbol, 'icon_thumb_url': asset.icon_thumb_url,
            'asset_type': asset.asset_type,
        } if asset else {'id': asset_id, 'name': asset_id, 'symbol': '', 'icon_thumb_url': None, 'asset_type': 'crypto'},
        'profile': {
            'current_price_idr': float(profile.current_price_idr) if profile and profile.current_price_idr else None,
            'price_change_24h': float(profile.price_change_24h) if profile and profile.price_change_24h else None,
        } if profile else None,
    }
    return jsonify(result), 201


@api_market_bp.route('/watchlist/<int:item_id>', methods=['DELETE'])
@login_required
def remove_from_watchlist(item_id):
    """Remove a watchlist item by its ID."""
    item = Watchlist.query.filter_by(id=item_id, user_id=get_current_user_id()).first()
    if not item:
        return jsonify({'error': 'Not in watchlist'}), 404

    db.session.delete(item)
    db.session.commit()
    return jsonify({'message': 'Removed from watchlist'})


@api_market_bp.route('/watchlist/<int:item_id>/move', methods=['PUT'])
@login_required
def move_watchlist_item(item_id):
    """Move a watchlist item to a different group.

    Body: { "group_id": 5 }  — or null to ungroup.
    """
    item = Watchlist.query.filter_by(id=item_id, user_id=get_current_user_id()).first()
    if not item:
        return jsonify({'error': 'Item not found'}), 404

    data = request.get_json() or {}
    new_group_id = data.get('group_id')  # Can be null

    # Check asset not already in target group
    if new_group_id != item.group_id:
        dup = Watchlist.query.filter_by(
            asset_id=item.asset_id, group_id=new_group_id, user_id=get_current_user_id()
        ).first()
        if dup:
            return jsonify({'error': 'Asset already in target group'}), 409

    old_group_id = item.group_id

    # Append to end of target group
    max_order = db.session.query(db.func.max(Watchlist.display_order)) \
        .filter_by(group_id=new_group_id, user_id=get_current_user_id()).scalar() or 0
    item.group_id = new_group_id
    item.display_order = max_order + 1
    db.session.commit()

    return jsonify({
        'item_id': item.id,
        'old_group_id': old_group_id,
        'new_group_id': new_group_id,
        'message': 'Moved successfully',
    })


@api_market_bp.route('/watchlist/<int:item_id>/notes', methods=['PUT'])
@login_required
def update_watchlist_notes(item_id):
    """Update notes for a watchlist item.

    Body: { "notes": "Target 100K" }
    """
    item = Watchlist.query.filter_by(id=item_id, user_id=get_current_user_id()).first()
    if not item:
        return jsonify({'error': 'Item not found'}), 404

    data = request.get_json() or {}
    item.notes = data.get('notes', '')
    db.session.commit()

    return jsonify({
        'message': 'Notes updated',
        'notes': item.notes,
    })


@api_market_bp.route('/watchlist/reorder', methods=['PUT'])
@login_required
def reorder_watchlist_items():
    """Reorder items within a group. Body: { order: [item_id1, item_id2, ...] }"""
    data = request.get_json() or {}
    order = data.get('order', [])
    for i, item_id in enumerate(order):
        Watchlist.query.filter_by(id=item_id, user_id=get_current_user_id()).update({'display_order': i})
    db.session.commit()
    return jsonify({'message': 'Reordered'})


# --- Watchlist Groups ---

@api_market_bp.route('/watchlist/groups', methods=['GET'])
@login_required
def get_watchlist_groups():
    """Get all watchlist groups ordered by display_order (item_count filtered by asset_mode)."""
    from app.helpers.asset_filter import get_asset_mode
    asset_mode = get_asset_mode()

    groups = WatchlistGroup.query.filter_by(user_id=get_current_user_id())\
        .order_by(WatchlistGroup.display_order.asc()).all()
    result = []
    for g in groups:
        gd = g.to_dict()
        gd['item_count'] = Watchlist.query.join(Asset, Watchlist.asset_id == Asset.id)\
            .filter(Watchlist.group_id == g.id, Watchlist.user_id == get_current_user_id(),
                    Asset.asset_type == asset_mode).count()
        result.append(gd)
    return jsonify(result)


@api_market_bp.route('/watchlist/groups', methods=['POST'])
@login_required
def create_watchlist_group():
    """Create a new watchlist group. Body: { name: "..." }"""
    data = request.get_json() or {}
    name = (data.get('name') or '').strip()
    if not name:
        return jsonify({'error': 'name is required'}), 400

    max_order = db.session.query(db.func.max(WatchlistGroup.display_order))\
        .filter(WatchlistGroup.user_id == get_current_user_id()).scalar() or 0
    group = WatchlistGroup(name=name, user_id=get_current_user_id(), display_order=max_order + 1)
    db.session.add(group)
    db.session.commit()
    return jsonify(group.to_dict()), 201


@api_market_bp.route('/watchlist/groups/<int:group_id>', methods=['PUT'])
@login_required
def update_watchlist_group(group_id):
    """Rename a watchlist group. Body: { name: "..." }"""
    group = WatchlistGroup.query.filter_by(id=group_id, user_id=get_current_user_id()).first_or_404()
    data = request.get_json() or {}
    name = (data.get('name') or '').strip()
    if name:
        group.name = name
    db.session.commit()
    return jsonify(group.to_dict())


@api_market_bp.route('/watchlist/groups/<int:group_id>', methods=['DELETE'])
@login_required
def delete_watchlist_group(group_id):
    """Delete a watchlist group and its items."""
    group = WatchlistGroup.query.filter_by(id=group_id, user_id=get_current_user_id()).first_or_404()
    # Delete all items in this group
    Watchlist.query.filter_by(group_id=group_id, user_id=get_current_user_id()).delete()
    db.session.delete(group)
    db.session.commit()
    return jsonify({'message': f'Group "{group.name}" deleted'})


@api_market_bp.route('/watchlist/groups/reorder', methods=['PUT'])
@login_required
def reorder_watchlist_groups():
    """Reorder groups. Body: { order: [group_id1, group_id2, ...] }"""
    data = request.get_json() or {}
    order = data.get('order', [])
    for i, gid in enumerate(order):
        WatchlistGroup.query.filter_by(id=gid, user_id=get_current_user_id()).update({'display_order': i})
    db.session.commit()
    return jsonify({'message': 'Groups reordered'})


@api_market_bp.route('/watchlist/groups/<int:group_id>/share', methods=['POST'])
@login_required
def share_watchlist_group(group_id):
    """Enable sharing for a watchlist group — generates a share_token."""
    import uuid
    group = WatchlistGroup.query.filter_by(id=group_id, user_id=get_current_user_id()).first_or_404()
    if group.is_shared and group.share_token:
        return jsonify({'share_token': group.share_token,
                        'share_url': f'/market/watchlist/shared/{group.share_token}',
                        'message': 'Group sudah di-share'})

    group.is_shared = True
    group.share_token = str(uuid.uuid4())
    db.session.commit()
    return jsonify({'share_token': group.share_token,
                    'share_url': f'/market/watchlist/shared/{group.share_token}',
                    'message': f'Group "{group.name}" berhasil di-share'})


@api_market_bp.route('/watchlist/groups/<int:group_id>/unshare', methods=['POST'])
@login_required
def unshare_watchlist_group(group_id):
    """Disable sharing for a watchlist group."""
    group = WatchlistGroup.query.filter_by(id=group_id, user_id=get_current_user_id()).first_or_404()
    group.is_shared = False
    group.share_token = None
    db.session.commit()
    return jsonify({'message': f'Sharing untuk group "{group.name}" dinonaktifkan'})


# --- Asset Import Endpoints (SSE streaming) ---

def _sse(data: dict) -> str:
    """Format dict as SSE event."""
    return f"data: {json.dumps(data)}\n\n"


@api_market_bp.route('/assets/import/markets', methods=['POST'])
@admin_required
def import_coins_markets():
    """Import assets from CoinGecko /assets/markets with SSE progress.

    Body: { pages: 1..20, per_page: 250 }
    Streams progress events as SSE.
    """
    from app.services.data_sync.coingecko import CoinGeckoProvider

    data = request.get_json() or {}
    pages = min(data.get('pages', 1), 20)
    per_page = min(data.get('per_page', 250), 250)
    total_expected = pages * per_page

    def generate():
        provider = CoinGeckoProvider()
        imported = 0
        updated = 0
        skipped = 0
        processed = 0

        yield _sse({'type': 'start', 'total': total_expected, 'pages': pages,
                     'msg': f'Mengambil top {total_expected:,} koin dari CoinGecko...'})

        for page in range(1, pages + 1):
            yield _sse({'type': 'progress', 'msg': f'Fetching halaman {page}/{pages}...',
                         'imported': imported, 'updated': updated, 'processed': processed,
                         'total': total_expected, 'pct': int(processed / total_expected * 100)})

            try:
                assets = provider.get_coins_markets(page=page, per_page=per_page)
            except Exception as e:
                yield _sse({'type': 'error', 'msg': f'Error halaman {page}: {str(e)}'})
                break

            if not assets:
                break

            from app.helpers.asset_id import make_asset_id
            for c in assets:
                raw_cid = c.get('id', '')
                sym = c.get('symbol', '')
                name = c.get('name', '')

                # Validate field lengths
                if not raw_cid or len(raw_cid) > 100 or not sym or len(sym) > 20 or not name or len(name) > 200:
                    processed += 1
                    continue

                cid = make_asset_id('COIN', raw_cid)
                existing = db.session.get(Asset, cid)
                if existing:
                    if c.get('market_cap_rank'):
                        existing.market_cap_rank = c['market_cap_rank']
                    if c.get('icon_thumb_url') and not existing.icon_thumb_url:
                        existing.icon_thumb_url = c['icon_thumb_url']
                    if c.get('image_url') and not existing.image_url:
                        existing.image_url = c['image_url']
                    updated += 1
                else:
                    asset = Asset(
                        id=cid, symbol=sym, name=name,
                        image_url=c.get('image_url'), icon_thumb_url=c.get('icon_thumb_url'),
                        market_cap_rank=c.get('market_cap_rank'), is_active=True,
                    )
                    db.session.add(asset)
                    mapping = AssetSourceMapping(
                        asset_id=cid, source='coingecko', source_asset_id=raw_cid,
                        source_url=f'https://www.coingecko.com/id/assets/{raw_cid}',
                        is_available=True,
                    )
                    db.session.add(mapping)
                    imported += 1
                processed += 1

            try:
                db.session.commit()
            except Exception:
                db.session.rollback()

            yield _sse({'type': 'progress',
                         'msg': f'Halaman {page}/{pages} selesai — {imported} baru, {updated} diupdate',
                         'imported': imported, 'updated': updated, 'processed': processed,
                         'total': total_expected, 'pct': int(processed / total_expected * 100)})

        yield _sse({'type': 'done', 'imported': imported, 'updated': updated,
                     'processed': processed, 'total': total_expected,
                     'msg': f'Selesai! {imported:,} koin baru, {updated:,} diupdate.'})

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


@api_market_bp.route('/assets/import/list', methods=['POST'])
@admin_required
def import_coins_list():
    """Import full asset list from CoinGecko /assets/list with SSE progress.

    Body: { include_platform: true }
    """
    from app.services.data_sync.coingecko import CoinGeckoProvider

    data = request.get_json() or {}
    include_platform = data.get('include_platform', True)

    def generate():
        provider = CoinGeckoProvider()

        yield _sse({'type': 'start', 'msg': 'Mengambil daftar koin dari CoinGecko...',
                     'total': 0, 'processed': 0, 'pct': 0})

        try:
            assets = provider.get_coins_list(include_platform=include_platform)
        except Exception as e:
            yield _sse({'type': 'error', 'msg': f'CoinGecko API error: {str(e)}'})
            return

        if not assets:
            yield _sse({'type': 'error', 'msg': 'Tidak ada data dari CoinGecko'})
            return

        total = len(assets)
        yield _sse({'type': 'progress', 'msg': f'Diterima {total:,} koin. Menyimpan ke database...',
                     'total': total, 'processed': 0, 'imported': 0, 'pct': 5})

        existing_ids = {c.id for c in Asset.query.with_entities(Asset.id).all()}
        imported = 0
        skipped = 0
        invalid = 0
        batch = []
        batch_mappings = []
        batch_size = 500

        from app.helpers.asset_id import make_asset_id
        for i, c in enumerate(assets):
            raw_cid = c.get('id', '')
            sym = c.get('symbol', '')
            name = c.get('name', '')

            # Validate field lengths against DB constraints
            if not raw_cid or len(raw_cid) > 100 or not sym or len(sym) > 20 or not name or len(name) > 200:
                invalid += 1
            else:
                cid = make_asset_id('COIN', raw_cid)
                if cid in existing_ids:
                    skipped += 1
                else:
                    batch.append(Asset(
                        id=cid, symbol=sym, name=name,
                        blockchain=c.get('blockchain'),
                        contract_address=c.get('contract_address'),
                        is_active=True,
                    ))
                    batch_mappings.append(AssetSourceMapping(
                        asset_id=cid, source='coingecko', source_asset_id=raw_cid,
                        source_url=f'https://www.coingecko.com/id/assets/{raw_cid}',
                        is_available=True,
                    ))
                    existing_ids.add(cid)
                    imported += 1

                if len(batch) >= batch_size:
                    try:
                        db.session.add_all(batch)
                        db.session.add_all(batch_mappings)
                        db.session.commit()
                    except Exception:
                        db.session.rollback()
                        imported -= len(batch)
                        invalid += len(batch)
                    batch = []
                    batch_mappings = []

            processed = i + 1
            # Send progress every 1000 items
            if processed % 1000 == 0 or processed == total:
                pct = int(processed / total * 100)
                yield _sse({'type': 'progress', 'processed': processed, 'total': total,
                             'imported': imported, 'skipped': skipped, 'invalid': invalid,
                             'pct': pct,
                             'msg': f'{processed:,}/{total:,} diproses — {imported:,} baru, {skipped:,} sudah ada'})

        if batch:
            try:
                db.session.add_all(batch)
                db.session.add_all(batch_mappings)
                db.session.commit()
            except Exception:
                db.session.rollback()
                imported -= len(batch)
                invalid += len(batch)

        msg = f'Selesai! {imported:,} koin baru dari {total:,} total. {skipped:,} sudah ada.'
        if invalid:
            msg += f' {invalid:,} dilewati (data tidak valid).'
        yield _sse({'type': 'done', 'imported': imported, 'skipped': skipped,
                     'invalid': invalid, 'processed': total, 'total': total, 'msg': msg})

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


@api_market_bp.route('/assets/import/idx', methods=['POST'])
@admin_required
def import_idx_stocks():
    """Import IDX stock listings from Yahoo Finance screener with SSE progress.

    Uses yfinance screener to fetch all stocks listed on JKT exchange.
    """
    import yfinance as yf

    def generate():
        page_size = 250

        yield _sse({'type': 'start', 'msg': 'Mengambil daftar saham IDX dari Yahoo Finance...',
                     'total': 0, 'processed': 0, 'pct': 0})

        # First request to get total
        try:
            eq = yf.EquityQuery('eq', ['exchange', 'JKT'])
            first_page = yf.screen(eq, size=page_size, offset=0)
            total = first_page.get('total', 0)
            all_quotes = first_page.get('quotes', [])
        except Exception as e:
            yield _sse({'type': 'error', 'msg': f'Yahoo Finance error: {str(e)}'})
            return

        if total == 0:
            yield _sse({'type': 'error', 'msg': 'Tidak ada data saham IDX'})
            return

        yield _sse({'type': 'progress', 'msg': f'Ditemukan {total:,} saham. Mengambil data...',
                     'total': total, 'processed': len(all_quotes), 'pct': 5})

        # Fetch remaining pages
        offset = page_size
        while offset < total:
            try:
                page_result = yf.screen(eq, size=page_size, offset=offset)
                quotes = page_result.get('quotes', [])
                if not quotes:
                    break
                all_quotes.extend(quotes)
                pct = min(int(len(all_quotes) / total * 50), 50)  # 0-50% for fetching
                yield _sse({'type': 'progress', 'processed': len(all_quotes),
                             'total': total, 'pct': pct,
                             'msg': f'Mengambil {len(all_quotes):,}/{total:,} saham...'})
            except Exception as e:
                yield _sse({'type': 'progress',
                             'msg': f'Warning: gagal ambil offset {offset}: {str(e)}',
                             'processed': len(all_quotes), 'total': total,
                             'pct': min(int(len(all_quotes) / total * 50), 50)})
                break
            offset += page_size

        yield _sse({'type': 'progress',
                     'msg': f'{len(all_quotes):,} saham diterima. Menyimpan ke database...',
                     'total': total, 'processed': len(all_quotes), 'pct': 50})

        # Save to database — with namespaced IDs, no more collision issues
        from app.helpers.asset_id import make_asset_id
        existing_ids = {c.id for c in Asset.query.with_entities(Asset.id).all()}
        existing_yahoo_mappings = {m.asset_id for m in AssetSourceMapping.query
                                   .filter_by(source='yahoo')
                                   .with_entities(AssetSourceMapping.asset_id).all()}
        imported = 0
        updated = 0
        skipped = 0
        batch = []
        batch_mappings = []
        batch_size = 100

        for i, q in enumerate(all_quotes):
            raw_symbol = q.get('symbol', '')
            if not raw_symbol:
                continue

            # Strip .JK suffix for raw ticker
            raw_ticker = raw_symbol.replace('.JK', '').upper()
            stock_id = make_asset_id('IDX', raw_ticker)
            name = q.get('longName') or q.get('shortName') or raw_ticker

            # Truncate to fit DB constraints
            if len(raw_ticker) > 20 or len(name) > 200:
                continue

            if stock_id in existing_ids:
                updated += 1  # Already exists
            else:
                asset = Asset(
                    id=stock_id,
                    symbol=raw_ticker,
                    name=name,
                    is_active=True,
                    asset_type='stock',
                    lot_size=100,
                )
                batch.append(asset)
                if stock_id not in existing_yahoo_mappings:
                    batch_mappings.append(AssetSourceMapping(
                        asset_id=stock_id, source='yahoo',
                        source_asset_id=f'{raw_ticker}.JK',
                        source_url=f'https://finance.yahoo.com/quote/{raw_ticker}.JK',
                        is_available=True,
                    ))
                    existing_yahoo_mappings.add(stock_id)
                existing_ids.add(stock_id)
                imported += 1

            if len(batch) >= batch_size:
                try:
                    db.session.add_all(batch)
                    db.session.add_all(batch_mappings)
                    db.session.commit()
                except Exception as e:
                    db.session.rollback()
                    imported -= len(batch)
                    logger.warning(f'[import_idx] batch commit failed: {e}')
                batch = []
                batch_mappings = []

            processed = i + 1
            if processed % 100 == 0 or processed == len(all_quotes):
                pct = 50 + int(processed / len(all_quotes) * 50)
                yield _sse({'type': 'progress', 'processed': processed, 'total': len(all_quotes),
                             'imported': imported, 'updated': updated, 'skipped': skipped,
                             'pct': pct,
                             'msg': f'{processed:,}/{len(all_quotes):,} diproses — {imported:,} baru, {updated:,} sudah ada'})

        # Flush remaining batch
        if batch:
            try:
                db.session.add_all(batch)
                db.session.add_all(batch_mappings)
                db.session.commit()
            except Exception as e:
                db.session.rollback()
                imported -= len(batch)
                logger.warning(f'[import_idx] final batch commit failed: {e}')

        msg = f'Selesai! {imported:,} saham baru dari {len(all_quotes):,} total.'
        if updated:
            msg += f' {updated:,} sudah ada.'
        yield _sse({'type': 'done', 'imported': imported, 'updated': updated,
                     'processed': len(all_quotes), 'total': len(all_quotes), 'msg': msg})

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


@api_market_bp.route('/assets/import/us', methods=['POST'])
@admin_required
def import_us_stocks():
    """Import US stock listings from Yahoo Finance screener with SSE progress.

    Uses yfinance screener to fetch stocks from NYSE (NYQ) and NASDAQ (NMS).
    """
    import yfinance as yf

    def generate():
        page_size = 250

        yield _sse({'type': 'start', 'msg': 'Mengambil daftar saham US dari Yahoo Finance...',
                     'total': 0, 'processed': 0, 'pct': 0})

        all_quotes = []
        exchange_map = {}  # symbol → exchange prefix for namespace
        seen_symbols = set()  # deduplicate across exchanges

        # Exchange code → namespace prefix mapping
        # NYQ=NYSE, NMS=NASDAQ Global Select, NGM=NASDAQ Global Market,
        # NCM=NASDAQ Capital Market, PCX=NYSE Arca, ASE=NYSE American (AMEX)
        exchanges = [
            ('NYQ', 'NYSE'),
            ('NMS', 'NASDAQ'),
            ('NGM', 'NASDAQ'),
            ('NCM', 'NASDAQ'),
            ('PCX', 'NYSE'),
            ('ASE', 'NYSE'),
        ]

        for exch_idx, (exch_code, exch_prefix) in enumerate(exchanges):
            try:
                eq = yf.EquityQuery('eq', ['exchange', exch_code])
                first_page = yf.screen(eq, size=page_size, offset=0)
                total_exch = first_page.get('total', 0)
                quotes = first_page.get('quotes', [])
                new_quotes = []
                for q in quotes:
                    sym = q.get('symbol', '')
                    if sym and sym not in seen_symbols:
                        seen_symbols.add(sym)
                        exchange_map[sym] = exch_prefix
                        new_quotes.append(q)
                all_quotes.extend(new_quotes)

                pct = int((exch_idx + 1) / len(exchanges) * 40)
                yield _sse({'type': 'progress',
                             'msg': f'{exch_code} ({exch_prefix}): {total_exch:,} ditemukan, {len(new_quotes)} unik. Total: {len(all_quotes):,}',
                             'total': 0, 'processed': len(all_quotes), 'pct': pct})

                # Fetch remaining pages for this exchange
                offset = page_size
                while offset < total_exch:
                    try:
                        page_result = yf.screen(eq, size=page_size, offset=offset)
                        page_quotes = page_result.get('quotes', [])
                        if not page_quotes:
                            break
                        new_quotes = []
                        for q in page_quotes:
                            sym = q.get('symbol', '')
                            if sym and sym not in seen_symbols:
                                seen_symbols.add(sym)
                                exchange_map[sym] = exch_prefix
                                new_quotes.append(q)
                        all_quotes.extend(new_quotes)
                        yield _sse({'type': 'progress', 'processed': len(all_quotes),
                                     'total': 0, 'pct': pct,
                                     'msg': f'{exch_code}: {len(all_quotes):,} saham diambil...'})
                    except Exception as e:
                        yield _sse({'type': 'progress',
                                     'msg': f'Warning: {exch_code} offset {offset}: {str(e)}',
                                     'processed': len(all_quotes), 'total': 0, 'pct': pct})
                        break
                    offset += page_size
            except Exception as e:
                yield _sse({'type': 'progress',
                             'msg': f'Warning: gagal ambil {exch_code}: {str(e)}',
                             'processed': len(all_quotes), 'total': 0, 'pct': 5})

        total = len(all_quotes)
        if total == 0:
            yield _sse({'type': 'error', 'msg': 'Tidak ada data saham US'})
            return

        yield _sse({'type': 'progress',
                     'msg': f'{total:,} saham US diterima. Menyimpan ke database...',
                     'total': total, 'processed': 0, 'pct': 50})

        # Save to database
        from app.helpers.asset_id import make_asset_id
        existing_ids = {c.id for c in Asset.query.with_entities(Asset.id).all()}
        existing_yahoo_mappings = {m.asset_id for m in AssetSourceMapping.query
                                   .filter_by(source='yahoo')
                                   .with_entities(AssetSourceMapping.asset_id).all()}
        imported = 0
        updated = 0
        skipped = 0
        batch = []
        batch_mappings = []
        batch_size = 100

        for i, q in enumerate(all_quotes):
            if not isinstance(q, dict):
                skipped += 1
                continue
            raw_symbol = q.get('symbol', '')
            if not raw_symbol:
                skipped += 1
                continue

            raw_ticker = raw_symbol.upper()
            # Detect exchange prefix
            prefix = exchange_map.get(raw_symbol, 'NYSE')
            stock_id = make_asset_id(prefix, raw_ticker)
            name = q.get('longName') or q.get('shortName') or raw_ticker

            # Truncate to fit DB constraints
            if len(raw_ticker) > 20 or len(name) > 200:
                continue

            if stock_id in existing_ids:
                updated += 1
            else:
                asset = Asset(
                    id=stock_id,
                    symbol=raw_ticker,
                    name=name,
                    is_active=True,
                    asset_type='stock_us',
                    lot_size=1,
                )
                batch.append(asset)
                if stock_id not in existing_yahoo_mappings:
                    batch_mappings.append(AssetSourceMapping(
                        asset_id=stock_id, source='yahoo',
                        source_asset_id=raw_ticker,
                        source_url=f'https://finance.yahoo.com/quote/{raw_ticker}',
                        is_available=True,
                    ))
                    existing_yahoo_mappings.add(stock_id)
                existing_ids.add(stock_id)
                imported += 1

            if len(batch) >= batch_size:
                try:
                    db.session.add_all(batch)
                    db.session.add_all(batch_mappings)
                    db.session.commit()
                except Exception as e:
                    db.session.rollback()
                    imported -= len(batch)
                    logger.warning(f'[import_us] batch commit failed: {e}')
                batch = []
                batch_mappings = []

            processed = i + 1
            if processed % 200 == 0 or processed == total:
                pct = 50 + int(processed / total * 50)
                yield _sse({'type': 'progress', 'processed': processed, 'total': total,
                             'imported': imported, 'updated': updated,
                             'pct': pct,
                             'msg': f'{processed:,}/{total:,} diproses — {imported:,} baru, {updated:,} sudah ada'})

        # Flush remaining batch
        if batch:
            try:
                db.session.add_all(batch)
                db.session.add_all(batch_mappings)
                db.session.commit()
            except Exception as e:
                db.session.rollback()
                imported -= len(batch)
                logger.warning(f'[import_us] final batch commit failed: {e}')

        if skipped:
            logger.warning(f'[import_us] skipped {skipped} quotes (empty symbol or invalid)')
        msg = f'Selesai! {imported:,} saham US baru dari {total:,} total.'
        if updated:
            msg += f' ({updated:,} sudah ada)'
        yield _sse({'type': 'done', 'imported': imported, 'updated': updated,
                     'processed': total, 'total': total, 'msg': msg})

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


# --- Asset Management Endpoints ---

@api_market_bp.route('/asset/manage', methods=['POST'])
@admin_required
def create_coin():
    """Create a new asset manually."""
    from app.helpers.asset_id import make_asset_id, prefix_for_asset_type, get_raw_id

    data = request.get_json() or {}
    raw_id = data.get('id', '').strip()
    symbol = data.get('symbol', '').strip().upper()
    name = data.get('name', '').strip()
    asset_type = data.get('asset_type', 'crypto')

    if not raw_id or not symbol or not name:
        return jsonify({'error': 'id, symbol, dan name wajib diisi'}), 400

    # Auto-prefix based on asset_type
    prefix = prefix_for_asset_type(asset_type)
    asset_id = make_asset_id(prefix, raw_id)

    existing = db.session.get(Asset, asset_id)
    if existing:
        return jsonify({'error': f'Koin "{asset_id}" sudah ada'}), 409

    asset = Asset(
        id=asset_id,
        symbol=symbol,
        name=name,
        asset_type=asset_type,
        lot_size=100 if asset_type == 'stock' else 1,
        website=data.get('website', '').strip() or None,
        blockchain=data.get('blockchain', '').strip() or None,
        contract_address=data.get('contract_address', '').strip() or None,
        is_active=True,
    )
    db.session.add(asset)

    # Auto-add source mapping based on asset_type
    if asset_type == 'stock':
        mapping = AssetSourceMapping(
            asset_id=asset_id, source='yahoo',
            source_asset_id=f'{raw_id.upper()}.JK',
            source_url=f'https://finance.yahoo.com/quote/{raw_id.upper()}.JK',
            is_available=True,
        )
    elif asset_type == 'stock_us':
        mapping = AssetSourceMapping(
            asset_id=asset_id, source='yahoo',
            source_asset_id=raw_id.upper(),
            source_url=f'https://finance.yahoo.com/quote/{raw_id.upper()}',
            is_available=True,
        )
    else:
        mapping = AssetSourceMapping(
            asset_id=asset_id, source='coingecko',
            source_asset_id=raw_id,
            source_url=f'https://www.coingecko.com/id/assets/{raw_id}',
            is_available=True,
        )
    db.session.add(mapping)
    db.session.commit()

    # Optionally sync after adding
    if data.get('sync_after'):
        try:
            from app.services.data_sync.sync_manager import SyncManager
            manager = SyncManager()
            manager.sync_coin(asset_id)
        except Exception:
            pass  # Sync failure shouldn't block asset creation

    return jsonify(asset.to_dict()), 201


@api_market_bp.route('/asset/<asset_id>/manage', methods=['PUT'])
@admin_required
def update_coin(asset_id):
    """Update asset details."""
    asset = Asset.query.get_or_404(asset_id)
    data = request.get_json() or {}

    if 'symbol' in data and data['symbol']:
        asset.symbol = data['symbol'].strip().upper()
    if 'name' in data and data['name']:
        asset.name = data['name'].strip()
    if 'website' in data:
        asset.website = data['website'].strip() or None
    if 'blockchain' in data:
        asset.blockchain = data['blockchain'].strip() or None
    if 'contract_address' in data:
        asset.contract_address = data['contract_address'].strip() or None
    if 'icon_thumb_url' in data:
        asset.icon_thumb_url = data['icon_thumb_url'].strip() or None
    if 'is_active' in data:
        asset.is_active = bool(data['is_active'])

    db.session.commit()
    return jsonify(asset.to_dict())


@api_market_bp.route('/asset/<asset_id>/manage', methods=['DELETE'])
@admin_required
def delete_coin(asset_id):
    """Delete a asset and all its data (cascade)."""
    asset = Asset.query.get_or_404(asset_id)
    coin_name = asset.name

    # SQLAlchemy cascade will handle related records
    db.session.delete(asset)
    db.session.commit()
    return jsonify({'message': f'{coin_name} berhasil dihapus beserta semua data terkait.'})


# --- Source Mapping Endpoints ---

@api_market_bp.route('/asset/<asset_id>/sources')
@login_required
def get_coin_sources(asset_id):
    """Get all source mappings for a asset."""
    asset = Asset.query.get_or_404(asset_id)
    mappings = AssetSourceMapping.query.filter_by(asset_id=asset_id).all()
    return jsonify({
        'asset_id': asset_id,
        'symbol': asset.symbol,
        'sources': [m.to_dict() for m in mappings],
    })


@api_market_bp.route('/asset/<asset_id>/sources', methods=['POST'])
@admin_required
def add_coin_source(asset_id):
    """Manually add a source mapping for a asset."""
    Asset.query.get_or_404(asset_id)
    data = request.get_json() or {}

    source = data.get('source')
    source_asset_id = data.get('source_asset_id')
    if not source or not source_asset_id:
        return jsonify({'error': 'source and source_asset_id are required'}), 400

    existing = AssetSourceMapping.query.filter_by(
        asset_id=asset_id, source=source
    ).first()

    if existing:
        existing.source_asset_id = source_asset_id
        existing.source_pair = data.get('source_pair', existing.source_pair)
        existing.source_url = data.get('source_url', existing.source_url)
        existing.is_available = True
        existing.last_checked = datetime.utcnow()
    else:
        existing = AssetSourceMapping(
            asset_id=asset_id,
            source=source,
            source_asset_id=source_asset_id,
            source_pair=data.get('source_pair'),
            source_url=data.get('source_url'),
            is_available=True,
            last_checked=datetime.utcnow(),
        )
        db.session.add(existing)

    db.session.commit()
    return jsonify(existing.to_dict()), 201


@api_market_bp.route('/asset/<asset_id>/sources/<source>', methods=['DELETE'])
@admin_required
def remove_coin_source(asset_id, source):
    """Remove a source mapping for a asset."""
    mapping = AssetSourceMapping.query.filter_by(
        asset_id=asset_id, source=source
    ).first()
    if not mapping:
        return jsonify({'error': 'Source mapping not found'}), 404

    db.session.delete(mapping)
    db.session.commit()
    return jsonify({'message': f'Removed {source} mapping for {asset_id}'})


# --- Icon Management Endpoints ---

@api_market_bp.route('/icons/stats')
@login_required
def icon_stats():
    """Get icon download statistics."""
    from app.services.icon_downloader import get_icon_stats
    return jsonify(get_icon_stats())


@api_market_bp.route('/icons/download/<asset_id>', methods=['POST'])
@admin_required
def download_single_icon(asset_id):
    """Download icon for a single asset."""
    from app.services.icon_downloader import (
        download_icon, download_us_stock_icon, get_local_icon_url,
        get_stock_remote_icon_url,
    )

    asset = Asset.query.get_or_404(asset_id)
    force = (request.args.get('force', '') == '1')

    # US stocks: use FMP + nvstly fallback chain
    if asset.asset_type == 'stock_us':
        if asset.icon_thumb_url:
            ok = download_icon(asset_id, asset.icon_thumb_url, force=force, asset_type='stock_us')
        else:
            ok = download_us_stock_icon(asset_id, asset.symbol or asset.id, force=force)
    elif asset.asset_type == 'stock':
        remote_url = asset.icon_thumb_url or get_stock_remote_icon_url(asset.symbol or asset.id)
        if not remote_url:
            return jsonify({'error': 'Koin ini tidak punya URL icon'}), 400
        ok = download_icon(asset_id, remote_url, force=force, asset_type='stock')
    else:
        remote_url = asset.icon_thumb_url or asset.image_url
        if not remote_url:
            return jsonify({'error': 'Koin ini tidak punya URL icon'}), 400
        ok = download_icon(asset_id, remote_url, force=force, asset_type='crypto')

    if ok:
        return jsonify({
            'asset_id': asset_id,
            'local_url': get_local_icon_url(asset_id, asset.asset_type),
            'message': 'Icon berhasil didownload.',
        })
    return jsonify({'error': 'Gagal download icon'}), 500


@api_market_bp.route('/icons/download/batch', methods=['POST'])
@admin_required
def download_icons_batch():
    """Batch download icons with SSE progress.

    Body: { scope: 'all' | 'missing' | 'page', asset_ids: [...], force: false }
    - 'all': download icons for ALL assets that have a remote URL
    - 'missing': only download for assets that don't have a local icon yet (default)
    - 'page': download for specific asset_ids provided
    """
    from app.services.icon_downloader import (
        download_icon, download_us_stock_icon, has_local_icon,
        get_stock_remote_icon_url,
    )

    data = request.get_json() or {}
    scope = data.get('scope', 'missing')
    force = data.get('force', False)
    requested_ids = data.get('asset_ids', [])

    def generate():
        # Build list of assets to process
        if scope == 'page' and requested_ids:
            assets = Asset.query.filter(Asset.id.in_(requested_ids)).all()
        else:
            assets = Asset.query.filter(
                or_(Asset.icon_thumb_url.isnot(None), Asset.image_url.isnot(None))
            ).all()
            # Also include stocks that can use stock icon sources
            stock_coins = Asset.query.filter(
                Asset.asset_type.in_(['stock', 'stock_us']),
                Asset.icon_thumb_url.is_(None),
            ).all()
            existing_ids = {c.id for c in assets}
            assets.extend(c for c in stock_coins if c.id not in existing_ids)

        # Filter to only those missing local icon (unless force)
        if scope == 'missing' and not force:
            assets = [c for c in assets if not has_local_icon(c.id, c.asset_type)]

        total = len(assets)
        yield _sse({'type': 'start', 'total': total,
                     'msg': f'Memulai download {total:,} icon...'})

        if total == 0:
            yield _sse({'type': 'done', 'downloaded': 0, 'failed': 0,
                         'skipped': 0, 'total': 0,
                         'msg': 'Tidak ada icon yang perlu didownload.'})
            return

        downloaded = 0
        failed = 0
        skipped = 0

        for i, asset in enumerate(assets):
            # US stocks: FMP + nvstly fallback chain
            if asset.asset_type == 'stock_us':
                if asset.icon_thumb_url:
                    ok = download_icon(asset.id, asset.icon_thumb_url, force=force,
                                       asset_type='stock_us')
                else:
                    ok = download_us_stock_icon(asset.id, asset.symbol or asset.id,
                                                force=force)
                if ok:
                    downloaded += 1
                else:
                    failed += 1
            elif asset.asset_type == 'stock':
                remote_url = asset.icon_thumb_url or get_stock_remote_icon_url(
                    asset.symbol or asset.id)
                if not remote_url:
                    skipped += 1
                else:
                    ok = download_icon(asset.id, remote_url, force=force,
                                       asset_type='stock')
                    if ok:
                        downloaded += 1
                    else:
                        failed += 1
            else:
                remote_url = asset.icon_thumb_url or asset.image_url
                if not remote_url:
                    skipped += 1
                else:
                    ok = download_icon(asset.id, remote_url, force=force,
                                       asset_type='crypto')
                    if ok:
                        downloaded += 1
                    else:
                        failed += 1

            processed = i + 1
            if processed % 50 == 0 or processed == total:
                pct = int(processed / total * 100)
                yield _sse({'type': 'progress', 'processed': processed,
                             'total': total, 'downloaded': downloaded,
                             'failed': failed, 'skipped': skipped, 'pct': pct,
                             'msg': f'{processed:,}/{total:,} — {downloaded:,} OK, {failed:,} gagal'})

        yield _sse({'type': 'done', 'downloaded': downloaded, 'failed': failed,
                     'skipped': skipped, 'total': total,
                     'msg': f'Selesai! {downloaded:,} icon didownload, {failed:,} gagal.'})

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


@api_market_bp.route('/icons/delete/<asset_id>', methods=['DELETE'])
@admin_required
def delete_single_icon(asset_id):
    """Delete locally stored icon for a asset."""
    from app.services.icon_downloader import delete_icon
    asset = Asset.query.get_or_404(asset_id)
    ok = delete_icon(asset_id, asset.asset_type)
    if ok:
        return jsonify({'message': f'Icon {asset_id} dihapus.'})
    return jsonify({'error': 'Icon tidak ditemukan'}), 404


# --- Full Sync Endpoint (SSE streaming) ---

@api_market_bp.route('/asset/<asset_id>/sync/full', methods=['POST'])
@admin_required
def sync_coin_full(asset_id):
    """Full sync for a single asset with SSE progress.

    Steps: 1) Profile + asset data, 2) Icon download, 3) OHLCV for each timeframe.
    Streams progress events as SSE.
    """
    from app.services.icon_downloader import (
        download_icon, download_us_stock_icon, has_local_icon,
        get_stock_remote_icon_url,
    )
    from app.services.data_sync.router import DataRouter

    asset = Asset.query.get_or_404(asset_id)
    coin_name = asset.name  # Capture before generator (avoids detached instance)
    coin_asset_type = asset.asset_type

    is_stock = coin_asset_type in ('stock', 'stock_us')

    def generate():
        steps = []
        steps.append({'key': 'profile', 'label': 'Profile & Market Data'})
        steps.append({'key': 'icon', 'label': 'Download Icon'})

        # Extended data steps for stocks only
        if is_stock:
            steps.append({'key': 'financials', 'label': 'Laporan Keuangan'})
            steps.append({'key': 'earnings', 'label': 'Earnings & Calendar'})
            steps.append({'key': 'dividends', 'label': 'Dividends & Splits'})
            steps.append({'key': 'analyst', 'label': 'Analyst Data'})
            steps.append({'key': 'ownership', 'label': 'Ownership & Insider'})
            steps.append({'key': 'news', 'label': 'Berita Terbaru'})
            steps.append({'key': 'options', 'label': 'Options Chain'})
            steps.append({'key': 'estimates', 'label': 'Estimasi Analis'})
            steps.append({'key': 'sustainability', 'label': 'ESG / Sustainability'})
            steps.append({'key': 'sec_filings', 'label': 'SEC Filings'})

        # Determine OHLCV timeframes (stock-aware)
        if coin_asset_type == 'stock_us':
            timeframes = AppSettings.get('stock_us_sync_timeframes', ['1h', '4h', '1D'])
        elif coin_asset_type == 'stock':
            timeframes = AppSettings.get('stock_sync_timeframes', ['1h', '4h', '1D'])
        else:
            timeframes = AppSettings.get('sync_timeframes', ['1h', '4h', '1D'])
        for tf in timeframes:
            steps.append({'key': f'ohlcv_{tf}', 'label': f'OHLCV {tf}'})

        total_steps = len(steps)
        results = {}

        yield _sse({
            'type': 'start',
            'asset_id': asset_id,
            'coin_name': coin_name,
            'steps': [{'key': s['key'], 'label': s['label']} for s in steps],
            'total_steps': total_steps,
            'msg': f'Memulai sync {coin_name}...',
        })

        router = DataRouter()
        # Use asset's asset_type to pick the right provider
        provider, provider_id = router.get_provider_for_coin(asset_id)
        current_step = 0

        # --- Step 1: Profile ---
        current_step += 1
        yield _sse({
            'type': 'step', 'step': current_step, 'total_steps': total_steps,
            'key': 'profile', 'status': 'running',
            'pct': int((current_step - 1) / total_steps * 100),
            'msg': 'Mengambil profile & market data...',
        })

        profile_ok = False
        profile_detail = ''
        try:
            profile_data = provider.get_coin_profile(asset_id)
            if profile_data:
                from app.services.data_sync.sync_manager import SyncManager
                manager = SyncManager()
                manager._upsert_coin(profile_data)
                manager._save_profile(asset_id, profile_data)
                manager._save_tickers(asset_id, profile_data.get('tickers', []))
                manager._update_source_mappings(asset_id)
                profile_ok = True
                price = profile_data.get('current_price_idr')
                rank = profile_data.get('market_cap_rank')
                profile_detail = f'Rank #{rank}' if rank else ''
                if price:
                    profile_detail += f', Rp {price:,.0f}' if profile_detail else f'Rp {price:,.0f}'
            else:
                profile_detail = 'Tidak ada data profile'
        except Exception as e:
            profile_detail = str(e)[:100]

        results['profile'] = {'ok': profile_ok, 'detail': profile_detail}
        yield _sse({
            'type': 'step', 'step': current_step, 'total_steps': total_steps,
            'key': 'profile', 'status': 'done' if profile_ok else 'error',
            'pct': int(current_step / total_steps * 100),
            'msg': f'Profile: {profile_detail}' if profile_detail else 'Profile selesai',
            'detail': profile_detail,
        })

        # --- Step 2: Icon ---
        current_step += 1
        yield _sse({
            'type': 'step', 'step': current_step, 'total_steps': total_steps,
            'key': 'icon', 'status': 'running',
            'pct': int((current_step - 1) / total_steps * 100),
            'msg': 'Mendownload icon...',
        })

        # Re-query asset from DB (profile may have updated icon_thumb_url)
        fresh_coin = db.session.get(Asset, asset_id)
        icon_ok = False
        icon_detail = ''
        if fresh_coin and fresh_coin.asset_type == 'stock_us':
            # US stocks: FMP + nvstly fallback
            if fresh_coin.icon_thumb_url:
                icon_ok = download_icon(asset_id, fresh_coin.icon_thumb_url,
                                        force=True, asset_type='stock_us')
            else:
                icon_ok = download_us_stock_icon(asset_id,
                                                  fresh_coin.symbol or asset_id,
                                                  force=True)
            icon_detail = 'Icon didownload' if icon_ok else 'Download gagal'
        elif fresh_coin and fresh_coin.asset_type == 'stock':
            remote_url = (fresh_coin.icon_thumb_url
                          or get_stock_remote_icon_url(fresh_coin.symbol or asset_id))
            if remote_url:
                icon_ok = download_icon(asset_id, remote_url, force=True,
                                        asset_type='stock')
                icon_detail = 'Icon didownload' if icon_ok else 'Download gagal'
            else:
                icon_detail = 'Tidak ada URL icon'
        elif fresh_coin:
            remote_url = fresh_coin.icon_thumb_url or fresh_coin.image_url
            if remote_url:
                icon_ok = download_icon(asset_id, remote_url, force=True,
                                        asset_type='crypto')
                icon_detail = 'Icon didownload' if icon_ok else 'Download gagal'
            else:
                icon_detail = 'Tidak ada URL icon'
        else:
            icon_detail = 'Asset tidak ditemukan'

        results['icon'] = {'ok': icon_ok, 'detail': icon_detail}
        yield _sse({
            'type': 'step', 'step': current_step, 'total_steps': total_steps,
            'key': 'icon', 'status': 'done' if icon_ok else 'skip',
            'pct': int(current_step / total_steps * 100),
            'msg': icon_detail,
            'detail': icon_detail,
        })

        # --- Extended Data Steps (stocks only) ---
        if is_stock:
            import time as _time
            from app.services.data_sync.sync_manager import SyncManager as _SM
            _mgr = _SM()

            # Step: Financials (6 types)
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'financials', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': 'Mengambil laporan keuangan...',
            })
            fin_ok = False
            fin_detail = ''
            try:
                fin_data = provider.get_financial_statements(asset_id)
                if fin_data:
                    _mgr._save_extended_batch(asset_id, fin_data)
                    fin_ok = True
                    fin_detail = f'{len(fin_data)} tipe data'
                else:
                    fin_detail = 'Tidak tersedia'
            except Exception as e:
                fin_detail = str(e)[:100]
            results['financials'] = {'ok': fin_ok, 'detail': fin_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'financials', 'status': 'done' if fin_ok else 'skip',
                'pct': int(current_step / total_steps * 100),
                'msg': f'Laporan Keuangan: {fin_detail}',
                'detail': fin_detail,
            })
            _time.sleep(0.3)

            # Step: Earnings
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'earnings', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': 'Mengambil data earnings...',
            })
            earn_ok = False
            earn_detail = ''
            try:
                earn_data = provider.get_earnings_data(asset_id)
                if earn_data:
                    _mgr._save_extended_batch(asset_id, earn_data)
                    earn_ok = True
                    earn_detail = f'{len(earn_data)} tipe data'
                else:
                    earn_detail = 'Tidak tersedia'
            except Exception as e:
                earn_detail = str(e)[:100]
            results['earnings'] = {'ok': earn_ok, 'detail': earn_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'earnings', 'status': 'done' if earn_ok else 'skip',
                'pct': int(current_step / total_steps * 100),
                'msg': f'Earnings: {earn_detail}',
                'detail': earn_detail,
            })
            _time.sleep(0.3)

            # Step: Dividends & Splits
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'dividends', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': 'Mengambil data dividen & splits...',
            })
            div_ok = False
            div_detail = ''
            try:
                div_data = provider.get_dividends_splits(asset_id)
                if div_data:
                    _mgr._save_extended_batch(asset_id, div_data)
                    div_ok = True
                    div_detail = f'{len(div_data)} tipe data'
                else:
                    div_detail = 'Tidak tersedia'
            except Exception as e:
                div_detail = str(e)[:100]
            results['dividends'] = {'ok': div_ok, 'detail': div_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'dividends', 'status': 'done' if div_ok else 'skip',
                'pct': int(current_step / total_steps * 100),
                'msg': f'Dividen: {div_detail}',
                'detail': div_detail,
            })
            _time.sleep(0.3)

            # Step: Analyst Data
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'analyst', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': 'Mengambil data analis...',
            })
            ana_ok = False
            ana_detail = ''
            try:
                ana_data = provider.get_analyst_data(asset_id)
                if ana_data:
                    _mgr._save_extended_batch(asset_id, ana_data)
                    ana_ok = True
                    ana_detail = f'{len(ana_data)} tipe data'
                else:
                    ana_detail = 'Tidak tersedia'
            except Exception as e:
                ana_detail = str(e)[:100]
            results['analyst'] = {'ok': ana_ok, 'detail': ana_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'analyst', 'status': 'done' if ana_ok else 'skip',
                'pct': int(current_step / total_steps * 100),
                'msg': f'Analis: {ana_detail}',
                'detail': ana_detail,
            })
            _time.sleep(0.3)

            # Step: Ownership & Insider
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'ownership', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': 'Mengambil data kepemilikan...',
            })
            own_ok = False
            own_detail = ''
            try:
                own_data = provider.get_ownership_data(asset_id)
                if own_data:
                    _mgr._save_extended_batch(asset_id, own_data)
                    own_ok = True
                    own_detail = f'{len(own_data)} tipe data'
                else:
                    own_detail = 'Tidak tersedia'
            except Exception as e:
                own_detail = str(e)[:100]
            results['ownership'] = {'ok': own_ok, 'detail': own_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'ownership', 'status': 'done' if own_ok else 'skip',
                'pct': int(current_step / total_steps * 100),
                'msg': f'Kepemilikan: {own_detail}',
                'detail': own_detail,
            })
            _time.sleep(0.3)

            # Step: News
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'news', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': 'Mengambil berita terbaru...',
            })
            news_ok = False
            news_detail = ''
            try:
                news_data = provider.get_news(asset_id)
                if news_data:
                    _mgr._save_extended_data(asset_id, 'news', news_data)
                    news_ok = True
                    news_detail = f'{len(news_data)} berita'
                else:
                    news_detail = 'Tidak tersedia'
            except Exception as e:
                news_detail = str(e)[:100]
            results['news'] = {'ok': news_ok, 'detail': news_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'news', 'status': 'done' if news_ok else 'skip',
                'pct': int(current_step / total_steps * 100),
                'msg': f'Berita: {news_detail}',
                'detail': news_detail,
            })
            _time.sleep(0.3)

            # Step: Options Chain
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'options', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': 'Mengambil data options chain...',
            })
            opt_ok = False
            opt_detail = ''
            try:
                opt_data = provider.get_options_data(asset_id)
                if opt_data:
                    _mgr._save_extended_batch(asset_id, opt_data)
                    opt_ok = True
                    dates_count = len(opt_data.get('options_dates', []))
                    chains_count = len(opt_data.get('options_chain', []))
                    opt_detail = f'{dates_count} dates, {chains_count} chains'
                else:
                    opt_detail = 'Tidak tersedia'
            except Exception as e:
                opt_detail = str(e)[:100]
            results['options'] = {'ok': opt_ok, 'detail': opt_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'options', 'status': 'done' if opt_ok else 'skip',
                'pct': int(current_step / total_steps * 100),
                'msg': f'Options: {opt_detail}',
                'detail': opt_detail,
            })
            _time.sleep(0.3)

            # Step: Estimates (growth, revenue, earnings, EPS)
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'estimates', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': 'Mengambil estimasi analis...',
            })
            est_ok = False
            est_detail = ''
            try:
                est_data = provider.get_estimate_data(asset_id)
                if est_data:
                    _mgr._save_extended_batch(asset_id, est_data)
                    est_ok = True
                    est_detail = f'{len(est_data)} tipe data'
                else:
                    est_detail = 'Tidak tersedia'
            except Exception as e:
                est_detail = str(e)[:100]
            results['estimates'] = {'ok': est_ok, 'detail': est_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'estimates', 'status': 'done' if est_ok else 'skip',
                'pct': int(current_step / total_steps * 100),
                'msg': f'Estimates: {est_detail}',
                'detail': est_detail,
            })
            _time.sleep(0.3)

            # Step: ESG / Sustainability
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'sustainability', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': 'Mengambil data ESG...',
            })
            sus_ok = False
            sus_detail = ''
            try:
                sus_data = provider.get_sustainability(asset_id)
                if sus_data:
                    _mgr._save_extended_batch(asset_id, sus_data)
                    sus_ok = True
                    sus_detail = 'ESG data tersedia'
                else:
                    sus_detail = 'Tidak tersedia'
            except Exception as e:
                sus_detail = str(e)[:100]
            results['sustainability'] = {'ok': sus_ok, 'detail': sus_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'sustainability', 'status': 'done' if sus_ok else 'skip',
                'pct': int(current_step / total_steps * 100),
                'msg': f'ESG: {sus_detail}',
                'detail': sus_detail,
            })
            _time.sleep(0.3)

            # Step: SEC Filings
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'sec_filings', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': 'Mengambil SEC filings...',
            })
            sec_ok = False
            sec_detail = ''
            try:
                sec_data = provider.get_sec_filings(asset_id)
                if sec_data:
                    _mgr._save_extended_batch(asset_id, sec_data)
                    sec_ok = True
                    count = len(sec_data.get('sec_filings', []))
                    sec_detail = f'{count} filings'
                else:
                    sec_detail = 'Tidak tersedia'
            except Exception as e:
                sec_detail = str(e)[:100]
            results['sec_filings'] = {'ok': sec_ok, 'detail': sec_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': 'sec_filings', 'status': 'done' if sec_ok else 'skip',
                'pct': int(current_step / total_steps * 100),
                'msg': f'SEC Filings: {sec_detail}',
                'detail': sec_detail,
            })

        # --- OHLCV per timeframe ---
        from datetime import datetime as dt
        now = dt.now()

        for tf in timeframes:
            current_step += 1
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': f'ohlcv_{tf}', 'status': 'running',
                'pct': int((current_step - 1) / total_steps * 100),
                'msg': f'Sync OHLCV {tf}...',
            })

            ohlcv_count = 0
            ohlcv_ok = False
            ohlcv_detail = ''
            try:
                from app.services.data_sync.sync_manager import SyncManager
                manager = SyncManager()
                ohlcv_count = manager._sync_ohlcv(asset_id, tf, provider_id, provider, now)
                ohlcv_ok = True
                ohlcv_detail = f'{ohlcv_count:,} records'
            except Exception as e:
                ohlcv_detail = str(e)[:100]

            results[f'ohlcv_{tf}'] = {'ok': ohlcv_ok, 'count': ohlcv_count, 'detail': ohlcv_detail}
            yield _sse({
                'type': 'step', 'step': current_step, 'total_steps': total_steps,
                'key': f'ohlcv_{tf}', 'status': 'done' if ohlcv_ok else 'error',
                'pct': int(current_step / total_steps * 100),
                'msg': f'OHLCV {tf}: {ohlcv_detail}',
                'detail': ohlcv_detail,
            })

        # --- Done ---
        ok_count = sum(1 for v in results.values() if v.get('ok'))
        total_count = len(results)
        all_ok = ok_count == total_count

        ohlcv_total = sum(v.get('count', 0) for k, v in results.items() if k.startswith('ohlcv_'))
        summary_parts = []
        if results['profile']['ok']:
            summary_parts.append('profile OK')
        if results['icon']['ok']:
            summary_parts.append('icon OK')
        if ohlcv_total > 0:
            summary_parts.append(f'{ohlcv_total:,} OHLCV')

        yield _sse({
            'type': 'done',
            'asset_id': asset_id,
            'all_ok': all_ok,
            'ok_count': ok_count,
            'total_count': total_count,
            'results': results,
            'pct': 100,
            'msg': (f'Sync {coin_name} selesai! ' + ', '.join(summary_parts)) if summary_parts else f'Sync {coin_name} selesai.',
        })

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


# --- Sync Scope Counts ---

@api_market_bp.route('/sync/scope-counts')
@login_required
def get_sync_scope_counts():
    """Get asset counts for each sync scope filter.

    Returns counts for: total_active, never_synced, stale_24h.
    Used by Sync All modal to show counts before sync starts.
    """
    total_active = apply_asset_filter(Asset.query.filter_by(is_active=True)).count()

    # Coins with NO OHLCV data at all
    coins_with_data = db.session.query(OHLCVData.asset_id).distinct().subquery()
    never_synced = apply_asset_filter(Asset.query.filter_by(is_active=True)).filter(
        ~Asset.id.in_(db.session.query(coins_with_data))
    ).count()

    # Coins not synced in last 24h (includes never_synced)
    cutoff = datetime.utcnow() - timedelta(hours=24)
    recently_synced_subq = db.session.query(
        OHLCVData.asset_id
    ).group_by(OHLCVData.asset_id).having(
        func.max(OHLCVData.created_at) >= cutoff
    ).subquery()
    stale = apply_asset_filter(Asset.query.filter_by(is_active=True)).filter(
        ~Asset.id.in_(db.session.query(recently_synced_subq))
    ).count()

    return jsonify({
        'total_active': total_active,
        'never_synced': never_synced,
        'stale_24h': stale,
    })


# --- Batch Sync Endpoint (SyncQueue + SSE) ---

@api_market_bp.route('/sync/batch', methods=['POST'])
@admin_required
def sync_batch():
    """Batch sync multiple assets with SyncQueue + SSE progress.

    Body: { asset_ids: [...], include_profile: true, include_icon: true,
            timeframes: ['1h','4h','1D'] | null,
            scope: 'never_synced'|'stale'|'', limit: 100 }
    If asset_ids is empty, resolves assets by scope (or all active by rank).
    timeframes=null uses saved setting, timeframes=[] skips OHLCV entirely.
    """
    data = request.get_json() or {}
    asset_ids = data.get('asset_ids', [])
    include_profile = data.get('include_profile', True)
    include_icon = data.get('include_icon', True)
    limit = min(int(data.get('limit', 200)), 500)  # Hard cap at 500
    scope = data.get('scope', '')

    # timeframes: null → use saved setting, [] → skip OHLCV, [...] → specific
    raw_tfs = data.get('timeframes')
    if raw_tfs is not None:
        valid_tfs = {'1m', '15m', '30m', '1h', '4h', '1D', '1W'}
        timeframes = [tf for tf in raw_tfs if tf in valid_tfs]
    else:
        timeframes = None  # will use AppSettings default

    # If no asset_ids, resolve by scope
    if not asset_ids:
        rank_order = (
            db.case((Asset.market_cap_rank.is_(None), 1), else_=0),
            Asset.market_cap_rank.asc(),
        )

        if scope == 'never_synced':
            # Active assets with NO OHLCV data
            coins_with_data = db.session.query(
                OHLCVData.asset_id
            ).distinct().subquery()
            query = apply_asset_filter(Asset.query.filter_by(is_active=True)).filter(
                ~Asset.id.in_(db.session.query(coins_with_data))
            ).order_by(*rank_order)
        elif scope == 'stale':
            # Active assets NOT synced in last 24h (includes never_synced)
            cutoff = datetime.utcnow() - timedelta(hours=24)
            recently_synced = db.session.query(
                OHLCVData.asset_id
            ).group_by(OHLCVData.asset_id).having(
                func.max(OHLCVData.created_at) >= cutoff
            ).subquery()
            query = apply_asset_filter(Asset.query.filter_by(is_active=True)).filter(
                ~Asset.id.in_(db.session.query(recently_synced))
            ).order_by(*rank_order)
        else:
            # Default: all active by rank
            query = apply_asset_filter(Asset.query.filter_by(is_active=True)).order_by(*rank_order)

        if limit > 0:
            query = query.limit(limit)
        asset_ids = [c.id for c in query.all()]

    if not asset_ids:
        return jsonify({'error': 'Tidak ada koin untuk di-sync'}), 400

    def generate():
        from app.services.data_sync.sync_manager import SyncManager
        manager = SyncManager()
        queue = manager.build_batch_queue(
            asset_ids,
            include_profile=include_profile,
            include_icon=include_icon,
            timeframes=timeframes,
        )

        for event in manager.execute_sync_queue(queue):
            yield _sse(event)

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


# --- Categories Endpoint ---

@api_market_bp.route('/categories')
@login_required
def get_categories():
    """Get top asset categories with counts.

    Returns sorted by asset count (desc). Used for filter chips.
    """
    limit = request.args.get('limit', 20, type=int)
    assets = apply_asset_filter(Asset.query.filter(
        Asset.is_active.is_(True),
        Asset.categories.isnot(None),
    )).all()
    cat_counts = {}
    for c in assets:
        if isinstance(c.categories, list):
            for cat in c.categories:
                cat_counts[cat] = cat_counts.get(cat, 0) + 1
    sorted_cats = sorted(cat_counts.items(), key=lambda x: x[1], reverse=True)[:limit]
    return jsonify([{'name': name, 'count': count} for name, count in sorted_cats])


# --- Screener Endpoint ---

@api_market_bp.route('/watchlist/toggle', methods=['POST'])
@login_required
def toggle_watchlist():
    """Toggle a asset in/out of watchlist. Returns new state.

    Body: { "asset_id": "bitcoin" }
    """
    data = request.get_json() or {}
    asset_id = data.get('asset_id')
    if not asset_id:
        return jsonify({'error': 'asset_id is required'}), 400

    existing = Watchlist.query.filter_by(asset_id=asset_id, user_id=get_current_user_id()).first()
    if existing:
        db.session.delete(existing)
        db.session.commit()
        return jsonify({'in_watchlist': False, 'asset_id': asset_id})

    max_order = db.session.query(db.func.max(Watchlist.display_order))\
        .filter(Watchlist.user_id == get_current_user_id()).scalar() or 0
    item = Watchlist(asset_id=asset_id, user_id=get_current_user_id(), display_order=max_order + 1)
    db.session.add(item)
    db.session.commit()
    return jsonify({'in_watchlist': True, 'asset_id': asset_id, 'item_id': item.id}), 201


@api_market_bp.route('/watchlist/check')
@login_required
def check_watchlist():
    """Check watchlist status for multiple asset IDs.

    Query: ?ids=bitcoin,ethereum,solana
    Returns: { "bitcoin": true, "ethereum": false, ... }
    """
    ids_str = request.args.get('ids', '')
    if not ids_str:
        return jsonify({})

    asset_ids = [cid.strip() for cid in ids_str.split(',') if cid.strip()][:100]
    in_wl = {w.asset_id for w in Watchlist.query.filter(
        Watchlist.user_id == get_current_user_id(),
        Watchlist.asset_id.in_(asset_ids)
    ).all()}

    return jsonify({cid: (cid in in_wl) for cid in asset_ids})


@api_market_bp.route('/screener')
@login_required
def screener_data():
    """Get screener data for a specific screener type.

    Query params:
        type: 'gainers', 'losers', 'volume', 'scalping', 'swing', 'investment'
        limit: Max items (default 50, max 200)
        page: Page number (default 1)
        sort: Column to sort by (price, 1h, 24h, 7d, volume, mcap, rank) — market types only
        dir: Sort direction ('asc' or 'desc')
        q: Search filter (name or symbol substring)
    """
    from app.models.signal import TradingSignal

    from app.helpers.asset_filter import get_asset_mode
    asset_mode = get_asset_mode()

    screener_type = request.args.get('type')
    valid_types = ('gainers', 'losers', 'volume', 'scalping', 'swing', 'investment', 'range_trading', 'early_bullish')
    if not screener_type or screener_type not in valid_types:
        return jsonify({'error': f'type is required. Valid: {", ".join(valid_types)}'}), 400

    limit = min(request.args.get('limit', 50, type=int), 200)
    page = request.args.get('page', 1, type=int)
    offset = (page - 1) * limit
    sort_by = request.args.get('sort', '')
    sort_dir = request.args.get('dir', 'desc')
    search_q = request.args.get('q', '').strip()

    if screener_type in ('gainers', 'losers', 'volume'):
        # --- Market Movers ---
        latest_sub = db.session.query(
            AssetProfile.asset_id,
            func.max(AssetProfile.id).label('max_id')
        ).group_by(AssetProfile.asset_id).subquery()

        base_query = db.session.query(AssetProfile, Asset).join(
            latest_sub, AssetProfile.id == latest_sub.c.max_id
        ).join(Asset, Asset.id == AssetProfile.asset_id
        ).filter(Asset.asset_type == asset_mode)

        # Search filter
        if search_q:
            base_query = base_query.filter(or_(
                Asset.name.ilike(f'%{search_q}%'),
                Asset.symbol.ilike(f'%{search_q}%'),
            ))

        # Category filter
        cat_filter = request.args.get('category', '').strip()
        if cat_filter:
            base_query = base_query.filter(
                Asset.categories.isnot(None),
                func.json_search(Asset.categories, 'one', cat_filter).isnot(None),
            )

        # Sort mapping
        sort_columns = {
            'price': AssetProfile.current_price_idr,
            '1h': AssetProfile.price_change_1h,
            '24h': AssetProfile.price_change_24h,
            '7d': AssetProfile.price_change_7d,
            '30d': AssetProfile.price_change_30d,
            'volume': AssetProfile.total_volume_idr,
            'mcap': AssetProfile.market_cap_idr,
            'rank': Asset.market_cap_rank,
        }

        count_query = base_query  # default before any filter that removes rows

        if sort_by and sort_by in sort_columns:
            col = sort_columns[sort_by]
            null_last = db.case((col.is_(None), 1), else_=0)
            order = col.asc() if sort_dir == 'asc' else col.desc()
            base_query = base_query.order_by(null_last, order)
        elif screener_type == 'gainers':
            base_query = base_query.filter(AssetProfile.price_change_24h.isnot(None))
            count_query = base_query
            base_query = base_query.order_by(AssetProfile.price_change_24h.desc())
        elif screener_type == 'losers':
            base_query = base_query.filter(AssetProfile.price_change_24h.isnot(None))
            count_query = base_query
            base_query = base_query.order_by(AssetProfile.price_change_24h.asc())
        else:  # volume
            base_query = base_query.order_by(
                db.case((AssetProfile.total_volume_idr.is_(None), 1), else_=0),
                AssetProfile.total_volume_idr.desc()
            )

        total = count_query.count()
        rows = base_query.offset(offset).limit(limit).all()

        items = []
        for idx, (profile, asset) in enumerate(rows):
            items.append({
                'rank': offset + idx + 1,
                'asset_id': asset.id,
                'name': asset.name,
                'symbol': asset.symbol,
                'icon_thumb_url': asset.icon_thumb_url,
                'asset_type': asset.asset_type,
                'current_price_idr': float(profile.current_price_idr) if profile.current_price_idr else None,
                'price_change_1h': float(profile.price_change_1h) if profile.price_change_1h else None,
                'price_change_24h': float(profile.price_change_24h) if profile.price_change_24h else None,
                'price_change_7d': float(profile.price_change_7d) if profile.price_change_7d else None,
                'price_change_30d': float(profile.price_change_30d) if profile.price_change_30d else None,
                'total_volume_idr': float(profile.total_volume_idr) if profile.total_volume_idr else None,
                'market_cap_idr': float(profile.market_cap_idr) if profile.market_cap_idr else None,
                'ath_idr': float(profile.ath_idr) if profile.ath_idr else None,
                'ath_distance': round((float(profile.current_price_idr) / float(profile.ath_idr) - 1) * 100, 2) if profile.ath_idr and profile.current_price_idr and float(profile.ath_idr) > 0 else None,
                'categories': asset.categories or [],
            })

        return jsonify({
            'type': screener_type, 'items': items,
            'total': total, 'page': page, 'limit': limit,
            'has_more': (offset + limit) < total,
        })

    elif screener_type == 'range_trading':
        # --- Range Trading Oscillation Scores ---
        from app.models.range_score import RangeTradingScore

        max_price = request.args.get('max_price', 0, type=float)
        min_cpd = request.args.get('min_cpd', 0, type=float)
        min_success = request.args.get('min_success', 0, type=float)
        quick_filter = request.args.get('filter', '')

        base_query = db.session.query(RangeTradingScore, Asset).join(
            Asset, Asset.id == RangeTradingScore.asset_id
        ).filter(
            Asset.asset_type == asset_mode,
            RangeTradingScore.oscillation_score > 0,
            RangeTradingScore.candle_count >= 30,
        )

        if max_price and max_price > 0:
            base_query = base_query.filter(
                RangeTradingScore.current_price_idr <= max_price
            )

        if search_q:
            base_query = base_query.filter(or_(
                Asset.name.ilike(f'%{search_q}%'),
                Asset.symbol.ilike(f'%{search_q}%'),
            ))

        # Cycle frequency filters
        if min_cpd > 0:
            base_query = base_query.filter(
                RangeTradingScore.cycles_per_day >= min_cpd
            )
        if min_success > 0:
            base_query = base_query.filter(
                RangeTradingScore.cycle_success_rate_pct >= min_success
            )

        # Quick filter presets
        if quick_filter == 'top_cyclers':
            base_query = base_query.filter(
                RangeTradingScore.cycles_per_day >= 1.0
            )
        elif quick_filter == 'high_success':
            base_query = base_query.filter(
                RangeTradingScore.cycle_success_rate_pct >= 60
            )

        total = base_query.count()

        # Sorting (server-side)
        sort_columns_rt = {
            'score': RangeTradingScore.oscillation_score,
            'cpd': RangeTradingScore.cycles_per_day,
            'duration': RangeTradingScore.avg_cycle_duration_hours,
            'success': RangeTradingScore.cycle_success_rate_pct,
            'range': RangeTradingScore.range_width_pct,
            'profit': RangeTradingScore.adaptive_est_profit_pct,
            'price': RangeTradingScore.current_price_idr,
            'bounces': RangeTradingScore.bounce_frequency,
        }

        if sort_by and sort_by in sort_columns_rt:
            col = sort_columns_rt[sort_by]
            # MariaDB NULLS LAST workaround
            null_last = db.case((col.is_(None), 1), else_=0)
            order = col.asc() if sort_dir == 'asc' else col.desc()
            base_query = base_query.order_by(null_last, order)
        else:
            base_query = base_query.order_by(
                RangeTradingScore.oscillation_score.desc()
            )

        rows = base_query.offset(offset).limit(limit).all()

        items = []
        for idx, (score, asset) in enumerate(rows):
            items.append({
                'rank': offset + idx + 1,
                'asset_id': asset.id,
                'name': asset.name,
                'symbol': asset.symbol,
                'icon_thumb_url': asset.icon_thumb_url,
                'asset_type': asset.asset_type,
                'current_price_idr': float(score.current_price_idr) if score.current_price_idr else None,
                'oscillation_score': float(score.oscillation_score),
                'is_mean_reverting': score.is_mean_reverting,
                'half_life_candles': float(score.half_life_candles) if score.half_life_candles else None,
                'mr_confidence': score.mr_confidence,
                'range_width_pct': float(score.range_width_pct) if score.range_width_pct else None,
                'nearest_support': float(score.nearest_support) if score.nearest_support else None,
                'nearest_resistance': float(score.nearest_resistance) if score.nearest_resistance else None,
                'bounce_frequency': score.bounce_frequency,
                'support_touches': score.support_touches,
                'resistance_touches': score.resistance_touches,
                'rsi_stability_pct': float(score.rsi_stability_pct) if score.rsi_stability_pct else None,
                'rsi_latest': float(score.rsi_latest) if score.rsi_latest else None,
                'atr_pct': float(score.atr_pct) if score.atr_pct else None,
                'bb_width': float(score.bb_width) if score.bb_width else None,
                'est_profit_per_cycle_pct': float(score.est_profit_per_cycle_pct) if score.est_profit_per_cycle_pct else None,
                'candle_count': score.candle_count,
                'computed_at': score.computed_at.isoformat() if score.computed_at else None,
                # --- Cycle frequency fields ---
                'cycles_per_day': float(score.cycles_per_day) if score.cycles_per_day else None,
                'avg_cycle_duration_hours': float(score.avg_cycle_duration_hours) if score.avg_cycle_duration_hours else None,
                'cycle_success_rate_pct': float(score.cycle_success_rate_pct) if score.cycle_success_rate_pct else None,
                'cycle_confidence': score.cycle_confidence,
                'cycle_complete_count': score.cycle_complete_count,
                'adaptive_support': float(score.adaptive_support) if score.adaptive_support else None,
                'adaptive_resistance': float(score.adaptive_resistance) if score.adaptive_resistance else None,
                'adaptive_range_pct': float(score.adaptive_range_pct) if score.adaptive_range_pct else None,
                'adaptive_est_profit_pct': float(score.adaptive_est_profit_pct) if score.adaptive_est_profit_pct else None,
                'adaptive_lookback': score.adaptive_lookback,
                'cycle_scanned_at': score.cycle_scanned_at.isoformat() if score.cycle_scanned_at else None,
            })

        return jsonify({
            'type': screener_type, 'items': items,
            'total': total, 'page': page, 'limit': limit,
            'has_more': (offset + limit) < total,
        })

    elif screener_type == 'early_bullish':
        # --- Early Bullish Momentum ---
        from app.models.bullish_score import BullishMomentumScore

        quick_filter = request.args.get('filter', '')

        base_query = db.session.query(BullishMomentumScore, Asset).join(
            Asset, Asset.id == BullishMomentumScore.asset_id
        ).filter(
            Asset.asset_type == asset_mode,
            BullishMomentumScore.candle_count >= 30,
        )

        # Default: show only EARLY_BULLISH unless filter overrides
        if quick_filter == 'all_phases':
            base_query = base_query.filter(BullishMomentumScore.score > 0)
        elif quick_filter == 'high_confidence':
            base_query = base_query.filter(
                BullishMomentumScore.bullish_phase == 'EARLY_BULLISH',
                BullishMomentumScore.confidence == 'High',
            )
        elif quick_filter == 'safe_only':
            base_query = base_query.filter(
                BullishMomentumScore.bullish_phase == 'EARLY_BULLISH',
                BullishMomentumScore.safety_rating.in_(['SAFE', 'MODERATE']),
            )
        elif quick_filter == 'strong_momentum':
            base_query = base_query.filter(
                BullishMomentumScore.bullish_phase == 'EARLY_BULLISH',
                BullishMomentumScore.momentum_state == 'ACCELERATING_UP',
            )
        elif quick_filter == 'ml_bullish':
            base_query = base_query.filter(
                BullishMomentumScore.bullish_phase == 'EARLY_BULLISH',
                BullishMomentumScore.ml_trend == 'bullish',
            )
        else:
            # Default: Early Bullish only
            base_query = base_query.filter(
                BullishMomentumScore.bullish_phase == 'EARLY_BULLISH',
            )

        if search_q:
            base_query = base_query.filter(or_(
                Asset.name.ilike(f'%{search_q}%'),
                Asset.symbol.ilike(f'%{search_q}%'),
            ))

        total = base_query.count()

        # Sort columns
        sort_columns_eb = {
            'score': BullishMomentumScore.score,
            'upside': BullishMomentumScore.upside_pct,
            'rsi': BullishMomentumScore.rsi_latest,
            'ml_pct': BullishMomentumScore.ml_short_pct,
            'conditions': BullishMomentumScore.conditions_met,
            'price': BullishMomentumScore.current_price_idr,
        }

        if sort_by and sort_by in sort_columns_eb:
            col = sort_columns_eb[sort_by]
            null_last = db.case((col.is_(None), 1), else_=0)
            order = col.asc() if sort_dir == 'asc' else col.desc()
            base_query = base_query.order_by(null_last, order)
        else:
            # Default sort: highest upside first
            base_query = base_query.order_by(
                db.case((BullishMomentumScore.upside_pct.is_(None), 1), else_=0),
                BullishMomentumScore.upside_pct.desc()
            )

        rows = base_query.offset(offset).limit(limit).all()

        items = []
        for idx, (bs, asset) in enumerate(rows):
            items.append({
                'rank': offset + idx + 1,
                'asset_id': asset.id,
                'name': asset.name,
                'symbol': asset.symbol,
                'icon_thumb_url': asset.icon_thumb_url,
                'asset_type': asset.asset_type,
                'current_price_idr': float(bs.current_price_idr) if bs.current_price_idr else None,
                'score': float(bs.score) if bs.score else None,
                'signal_type': bs.signal_type,
                'confidence': bs.confidence,
                'safety_rating': bs.safety_rating,
                'bullish_phase': bs.bullish_phase,
                'conditions_met': bs.conditions_met,
                'momentum_state': bs.momentum_state,
                'velocity_short_pct': float(bs.velocity_short_pct) if bs.velocity_short_pct else None,
                'ml_trend': bs.ml_trend,
                'ml_short_pct': float(bs.ml_short_pct) if bs.ml_short_pct else None,
                'ml_medium_pct': float(bs.ml_medium_pct) if bs.ml_medium_pct else None,
                'ml_target_price': float(bs.ml_target_price) if bs.ml_target_price else None,
                'rsi_latest': float(bs.rsi_latest) if bs.rsi_latest else None,
                'bb_position': bs.bb_position,
                'zscore': float(bs.zscore) if bs.zscore else None,
                'ema_bullish': bs.ema_bullish,
                'entry_price': float(bs.entry_price) if bs.entry_price else None,
                'stop_loss': float(bs.stop_loss) if bs.stop_loss else None,
                'take_profit_1': float(bs.take_profit_1) if bs.take_profit_1 else None,
                'take_profit_2': float(bs.take_profit_2) if bs.take_profit_2 else None,
                'upside_pct': float(bs.upside_pct) if bs.upside_pct else None,
                'computed_at': bs.computed_at.isoformat() if bs.computed_at else None,
            })

        return jsonify({
            'type': screener_type, 'items': items,
            'total': total, 'page': page, 'limit': limit,
            'has_more': (offset + limit) < total,
        })

    else:
        # --- Strategy Signals ---
        strat_map = {
            'scalping': ['scalping'],
            'swing': ['swing', 'short_term', 'medium_term'],
            'investment': ['long_term'],
        }
        strategy_enums = strat_map[screener_type]

        base_query = db.session.query(TradingSignal, Asset).join(
            Asset, Asset.id == TradingSignal.asset_id
        ).filter(
            Asset.asset_type == asset_mode,
            TradingSignal.status == 'active',
            TradingSignal.recommended_strategy.in_(strategy_enums)
        )

        total = base_query.count()

        # Fallback for investment: if no long_term, show BUY+SAFE/MODERATE
        if total == 0 and screener_type == 'investment':
            base_query = db.session.query(TradingSignal, Asset).join(
                Asset, Asset.id == TradingSignal.asset_id
            ).filter(
                Asset.asset_type == asset_mode,
                TradingSignal.status == 'active',
                TradingSignal.signal_type == 'BUY',
                TradingSignal.safety_rating.in_(['SAFE', 'MODERATE']),
            )
            total = base_query.count()

        base_query = base_query.order_by(TradingSignal.score.desc())
        rows = base_query.offset(offset).limit(limit).all()

        # Batch-load current prices
        sig_asset_ids = list({asset.id for _, asset in rows})
        price_map = {}
        if sig_asset_ids:
            price_sub = db.session.query(
                AssetProfile.asset_id,
                func.max(AssetProfile.id).label('max_id')
            ).filter(AssetProfile.asset_id.in_(sig_asset_ids)
            ).group_by(AssetProfile.asset_id).subquery()
            price_profiles = AssetProfile.query.join(
                price_sub, AssetProfile.id == price_sub.c.max_id
            ).all()
            price_map = {
                p.asset_id: float(p.current_price_idr) if p.current_price_idr else None
                for p in price_profiles
            }

        items = []
        for idx, (signal, asset) in enumerate(rows):
            items.append({
                'rank': offset + idx + 1,
                'signal_id': signal.id,
                'asset_id': asset.id,
                'name': asset.name,
                'symbol': asset.symbol,
                'icon_thumb_url': asset.icon_thumb_url,
                'asset_type': asset.asset_type,
                'signal_type': signal.signal_type,
                'score': float(signal.score),
                'confidence': signal.confidence,
                'safety_rating': signal.safety_rating,
                'recommended_strategy': signal.recommended_strategy,
                'entry_price': float(signal.entry_price) if signal.entry_price else None,
                'stop_loss': float(signal.stop_loss) if signal.stop_loss else None,
                'take_profit_1': float(signal.take_profit_1) if signal.take_profit_1 else None,
                'current_price_idr': price_map.get(asset.id),
                'created_at': signal.created_at.isoformat() if signal.created_at else None,
            })

        return jsonify({
            'type': screener_type, 'items': items,
            'total': total, 'page': page, 'limit': limit,
            'has_more': (offset + limit) < total,
        })


@api_market_bp.route('/screener/range-trading/scan', methods=['POST'])
@admin_required
def scan_range_trading():
    """Scan assets for range-trading suitability with SSE progress.

    Body (optional JSON):
        max_price_idr: Max price filter (default: no filter = scan all)
        timeframe: OHLCV timeframe (default: '1H')
    """
    from app.services.oscillation import OscillationAnalyzer

    data = request.get_json(silent=True) or {}
    max_price = data.get('max_price_idr')
    timeframe = data.get('timeframe', '1H')

    def generate():
        analyzer = OscillationAnalyzer()
        for event in analyzer.scan_all_coins_sse(max_price, timeframe):
            yield _sse(event)

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


@api_market_bp.route('/screener/range-trading/scan-cycles', methods=['POST'])
@admin_required
def scan_cycle_frequency_all():
    """Scan assets for cycle frequency with SSE progress.

    Body (optional JSON):
        mode: 'agresif' | 'moderate' | 'santai' (default: 'agresif')
    """
    from app.services.oscillation import OscillationAnalyzer

    data = request.get_json(silent=True) or {}
    mode = data.get('mode', 'agresif')

    def generate():
        analyzer = OscillationAnalyzer()
        for event in analyzer.scan_cycle_frequency_sse(mode):
            yield _sse(event)

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


@api_market_bp.route('/screener/early-bullish/scan', methods=['POST'])
@admin_required
def scan_early_bullish():
    """Scan assets for early bullish momentum with SSE progress.

    Body (optional JSON):
        timeframe: OHLCV timeframe (default: '1H')
    """
    from app.services.bullish_screener import BullishScreener

    data = request.get_json(silent=True) or {}
    timeframe = data.get('timeframe', '1H')

    def generate():
        screener = BullishScreener()
        for event in screener.scan_all_coins_sse(timeframe):
            yield _sse(event)

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


@api_market_bp.route('/screener/signals/scan', methods=['POST'])
@admin_required
def scan_signals_all():
    """Scan all assets and generate trading signals with SSE progress.

    Body (optional JSON):
        timeframe: OHLCV timeframe (default: '1D')
    """
    from app.services.scorer import ScoringService

    data = request.get_json(silent=True) or {}
    timeframe = data.get('timeframe', '1D')

    def generate():
        scorer = ScoringService()
        for event in scorer.scan_signals_sse(timeframe):
            yield _sse(event)

    return Response(stream_with_context(generate()),
                    mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


@api_market_bp.route('/asset/<asset_id>/range-rescan', methods=['POST'])
@login_required
def rescan_range_trading_coin(asset_id):
    """Re-scan a single asset for range-trading suitability.

    Runs OscillationAnalyzer on this asset and upserts the RangeTradingScore.
    Returns fresh metrics as JSON.
    """
    from app.helpers.ml_mode import is_view_only

    # View-only: return existing RangeTradingScore from DB
    if is_view_only():
        from app.models.range_score import RangeTradingScore as RTS
        existing = RTS.query.filter_by(asset_id=asset_id).first()
        if existing:
            return jsonify({
                'asset_id': asset_id,
                'oscillation_score': float(existing.oscillation_score) if existing.oscillation_score else 0,
                'message': f'Cached score: {existing.oscillation_score}',
                'ml_mode': 'view_only',
                'cached': True,
                'cached_at': existing.computed_at.isoformat() if existing.computed_at else None,
            })
        return jsonify({
            'asset_id': asset_id,
            'oscillation_score': 0,
            'message': 'Belum ada data range-trading. Jalankan bulk scan dari Admin ML.',
            'ml_mode': 'view_only',
            'cached': True,
        })

    from app.services.oscillation import OscillationAnalyzer
    from app.models.range_score import RangeTradingScore
    from app.models.asset import AssetProfile
    from datetime import datetime
    import pandas as pd

    asset = Asset.query.get_or_404(asset_id)
    from app.helpers.asset_filter import get_source_for_coin, get_fees
    source = get_source_for_coin(asset_id)
    fees = get_fees()
    buy_fee = fees['buy_fee_pct']
    sell_fee = fees['sell_fee_pct']
    timeframe = request.args.get('timeframe', '1h')

    # Load OHLCV — fallback to best available timeframe if requested has no data
    records = OHLCVData.query.filter_by(
        asset_id=asset_id, timeframe=timeframe, source=source,
    ).order_by(OHLCVData.datetime_wib.asc()).all()

    if len(records) < 40:
        # Try fallback timeframes
        from sqlalchemy import func as sa_func
        tf_rows = db.session.query(
            OHLCVData.timeframe, sa_func.count(OHLCVData.id)
        ).filter_by(asset_id=asset_id, source=source
        ).group_by(OHLCVData.timeframe).all()
        preferred = ['1D', '4h', '1h', '30m', '15m', '1W', '1m']
        for tf in preferred:
            cnt = next((c for t, c in tf_rows if t == tf), 0)
            if cnt >= 40:
                timeframe = tf
                records = OHLCVData.query.filter_by(
                    asset_id=asset_id, timeframe=tf, source=source,
                ).order_by(OHLCVData.datetime_wib.asc()).all()
                break

    if len(records) < 40:
        return jsonify({'error': f'Data OHLCV kurang ({len(records)} candles, butuh 40+)'}), 400

    # Build DataFrame
    data = [{
        'timestamp': r.timestamp,
        'datetime': r.datetime_wib,
        'open': float(r.open),
        'high': float(r.high),
        'low': float(r.low),
        'close': float(r.close),
        'volume': float(r.volume),
    } for r in records]
    ohlcv_df = pd.DataFrame(data)

    # Get current price
    profile = AssetProfile.query.filter_by(asset_id=asset_id)\
        .order_by(AssetProfile.fetched_at.desc()).first()
    price = float(profile.current_price_idr) if profile and profile.current_price_idr \
        else float(ohlcv_df['close'].iloc[-1])

    # Analyze
    analyzer = OscillationAnalyzer()
    metrics = analyzer.analyze_coin(asset_id, ohlcv_df, price,
                                     buy_fee_pct=buy_fee, sell_fee_pct=sell_fee)

    if not metrics:
        return jsonify({'error': 'Analysis returned no metrics'}), 400

    # Upsert
    existing = RangeTradingScore.query.filter_by(asset_id=asset_id).first()
    if existing:
        for key, val in metrics.items():
            setattr(existing, key, val)
        existing.timeframe = timeframe
        existing.computed_at = datetime.utcnow()
    else:
        existing = RangeTradingScore(
            asset_id=asset_id,
            timeframe=timeframe,
            computed_at=datetime.utcnow(),
            **metrics,
        )
        db.session.add(existing)

    db.session.commit()

    return jsonify({
        'asset_id': asset_id,
        'oscillation_score': metrics['oscillation_score'],
        'message': f'Rescan selesai. Score: {metrics["oscillation_score"]}',
    })


@api_market_bp.route('/asset/<asset_id>/cycle-frequency')
@login_required
def get_cycle_frequency(asset_id):
    """Compute cycle frequency with adaptive S/R per mode.

    Query params:
        mode: 'agresif' | 'moderate' | 'santai' (preferred)
        timeframe: '1h' | '4h' | '1D' (backward compat → mapped to mode)

    Recomputes S/R on-the-fly with mode-appropriate lookback window
    for tighter, more relevant support/resistance levels.
    """
    from app.helpers.ml_mode import is_view_only

    # View-only: return existing cycle data from RangeTradingScore
    if is_view_only():
        from app.models.range_score import RangeTradingScore as RTS
        existing = RTS.query.filter_by(asset_id=asset_id).first()
        if existing and existing.cycle_complete_count is not None:
            return jsonify({
                'asset_id': asset_id,
                'ml_mode': 'view_only',
                'cached': True,
                'cached_at': existing.cycle_scanned_at.isoformat() if existing.cycle_scanned_at else None,
                'cycles_per_day': float(existing.cycles_per_day) if existing.cycles_per_day else 0,
                'avg_cycle_duration_hours': float(existing.avg_cycle_duration_hours) if existing.avg_cycle_duration_hours else 0,
                'cycle_success_rate_pct': float(existing.cycle_success_rate_pct) if existing.cycle_success_rate_pct else 0,
                'cycle_complete_count': existing.cycle_complete_count or 0,
                'cycle_failed_count': existing.cycle_failed_count or 0,
                'cycle_confidence': existing.cycle_confidence or 'INSUFFICIENT',
                'adaptive_support': float(existing.adaptive_support) if existing.adaptive_support else None,
                'adaptive_resistance': float(existing.adaptive_resistance) if existing.adaptive_resistance else None,
                'adaptive_range_pct': float(existing.adaptive_range_pct) if existing.adaptive_range_pct else None,
                'adaptive_est_profit_pct': float(existing.adaptive_est_profit_pct) if existing.adaptive_est_profit_pct else None,
            })
        return jsonify({
            'asset_id': asset_id,
            'ml_mode': 'view_only',
            'cached': True,
            'message': 'Belum ada data cycle frequency. Jalankan bulk scan dari Admin ML.',
            'cycles_per_day': 0,
            'cycle_confidence': 'INSUFFICIENT',
        })

    from app.services.oscillation import OscillationAnalyzer, ADAPTIVE_PROFILES, VALID_MODES
    import pandas as pd

    asset = Asset.query.get_or_404(asset_id)

    # Determine mode: explicit mode param preferred, fallback to timeframe
    mode = request.args.get('mode', '')
    if not mode:
        tf = request.args.get('timeframe', '1h')
        tf_mode_map = {'1h': 'agresif', '4h': 'moderate', '1D': 'santai'}
        mode = tf_mode_map.get(tf, 'agresif')
    if mode not in VALID_MODES:
        return jsonify({'error': f'Mode tidak valid: {mode}. Gunakan: {", ".join(sorted(VALID_MODES))}'}), 400

    # Determine which timeframes are needed for this mode
    needed_tfs = set(p['timeframe'] for p in ADAPTIVE_PROFILES[mode])

    # Load OHLCV data for needed timeframes
    from app.helpers.asset_filter import get_source_for_coin, get_fees as _get_fees
    source = get_source_for_coin(asset_id)
    _f = _get_fees()
    buy_fee = _f['buy_fee_pct']
    sell_fee = _f['sell_fee_pct']

    ohlcv_map: dict[str, pd.DataFrame] = {}
    for tf in needed_tfs:
        records = OHLCVData.query.filter_by(
            asset_id=asset_id, timeframe=tf, source=source,
        ).order_by(OHLCVData.datetime_wib.asc()).all()

        if len(records) < 20:  # absolute minimum for smallest window
            continue

        data = [{
            'timestamp': r.timestamp,
            'datetime': r.datetime_wib,
            'open': float(r.open),
            'high': float(r.high),
            'low': float(r.low),
            'close': float(r.close),
            'volume': float(r.volume),
        } for r in records]
        ohlcv_map[tf] = pd.DataFrame(data)

    if not ohlcv_map:
        return jsonify({'error': f'Data OHLCV kurang untuk mode {mode}. '
                                 f'Butuh minimal 20 candles di timeframe: {", ".join(needed_tfs)}.'}), 400

    # Get current price
    price_record = OHLCVData.query.filter_by(
        asset_id=asset_id, timeframe='1D', source=source,
    ).order_by(OHLCVData.datetime_wib.desc()).first()
    if not price_record:
        # Fallback: use 1h latest
        price_record = OHLCVData.query.filter_by(
            asset_id=asset_id, timeframe='1h', source=source,
        ).order_by(OHLCVData.datetime_wib.desc()).first()
    current_price = float(price_record.close) if price_record else 0
    if current_price <= 0:
        return jsonify({'error': 'Harga saat ini tidak tersedia.'}), 400

    # Run adaptive cycle frequency
    analyzer = OscillationAnalyzer()
    _asset_type = 'stock' if asset_id.startswith('IDX.') else 'crypto'
    result = analyzer.compute_cycle_frequency_adaptive(
        asset_id=asset_id,
        ohlcv_map=ohlcv_map,
        mode=mode,
        current_price=current_price,
        buy_fee_pct=buy_fee,
        sell_fee_pct=sell_fee,
        asset_type=_asset_type,
    )

    if result.get('error'):
        return jsonify(result), 400

    return jsonify(result)


@api_market_bp.route('/recommendations/buy')
@login_required
def get_buy_recommendations():
    """Get ranked buy recommendations with profit projections.

    Query params:
        safety: 'all', 'safe' (default: 'all')
        min_success: Min success rate % (default: 0)
        min_cpd: Min cycles/day (default: 0)
        sort: 'score', 'daily_ev', 'fastest', 'safest', 'success_rate' (default: 'score')
        limit: Max items (default: 20, max 100)
        page: Page number (default: 1)
        q: Search filter
        capital: Capital IDR for projections (default: 1000000)
    """
    from app.services.buy_recommender import BuyRecommender

    recommender = BuyRecommender()
    result = recommender.get_recommendations(
        safety_filter=request.args.get('safety', 'all'),
        min_success_rate=request.args.get('min_success', 0, type=float),
        min_cycles_per_day=request.args.get('min_cpd', 0, type=float),
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        capital_idr=request.args.get('capital', 1_000_000, type=float),
    )
    return jsonify(result)


@api_market_bp.route('/screener/oversold-bounce')
@login_required
def get_oversold_bounce_candidates():
    """Get oversold bounce screener candidates."""
    from app.services.oversold_bounce_screener import OversoldBounceScreener
    screener = OversoldBounceScreener()
    result = screener.get_candidates(
        tier_filter=request.args.get('tier', 'all'),
        min_score=request.args.get('min_score', 0, type=float),
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


@api_market_bp.route('/screener/breakout')
@login_required
def get_breakout_candidates():
    """Get breakout detector candidates."""
    from app.services.breakout_detector import BreakoutDetector
    screener = BreakoutDetector()
    result = screener.get_candidates(
        tier_filter=request.args.get('tier', 'all'),
        min_score=request.args.get('min_score', 0, type=float),
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


@api_market_bp.route('/screener/passive-income')
@login_required
def get_passive_income_candidates():
    """Get passive income screener candidates."""
    from app.services.passive_income_screener import PassiveIncomeScreener
    screener = PassiveIncomeScreener()
    result = screener.get_candidates(
        tier_filter=request.args.get('tier', 'all'),
        min_score=request.args.get('min_score', 0, type=float),
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
        capital_idr=request.args.get('capital', 1_000_000, type=float),
    )
    return jsonify(result)


@api_market_bp.route('/screener/smart-accumulation')
@login_required
def get_smart_accumulation_candidates():
    """Get smart accumulation screener candidates."""
    from app.services.smart_accumulation_screener import SmartAccumulationScreener
    screener = SmartAccumulationScreener()
    result = screener.get_candidates(
        tier_filter=request.args.get('tier', 'all'),
        min_score=request.args.get('min_score', 0, type=float),
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


@api_market_bp.route('/screener/swing-trade')
@login_required
def get_swing_trade_candidates():
    """Get swing trade screener candidates."""
    from app.services.swing_trade_screener import SwingTradeScreener
    screener = SwingTradeScreener()
    result = screener.get_candidates(
        tier_filter=request.args.get('tier', 'all'),
        min_score=request.args.get('min_score', 0, type=float),
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


# ─── Sell Signal / Exit Screener ──────────────────────────────────────────────

@api_market_bp.route('/screener/sell-signal')
@login_required
def get_sell_signal_candidates():
    """Get ranked sell signal / exit candidates — high score = stronger sell signal."""
    from app.services.sell_signal_screener import SellSignalScreener
    screener = SellSignalScreener()
    result = screener.get_candidates(
        tier_filter=request.args.get('tier', 'all'),
        min_score=request.args.get('min_score', 0, type=float),
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


# ─── Portfolio Health Monitor ─────────────────────────────────────────────────

@api_market_bp.route('/screener/portfolio-health')
@login_required
def get_portfolio_health():
    """Get portfolio health status for all assets — low score = needs attention."""
    from app.services.portfolio_health_monitor import PortfolioHealthMonitor
    monitor = PortfolioHealthMonitor()
    result = monitor.get_candidates(
        health_filter=request.args.get('health', 'all'),
        min_score=request.args.get('min_score', 0, type=float),
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


# ─── Meta-Screener / Unified Ranking ─────────────────────────────────────────

@api_market_bp.route('/screener/meta')
@login_required
def get_meta_screener():
    """Get unified meta-screener ranking — combines all 6 screeners."""
    from app.services.meta_screener import MetaScreener
    screener = MetaScreener()
    result = screener.get_candidates(
        tier_filter=request.args.get('tier', 'all'),
        min_score=request.args.get('min_score', 0, type=float),
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


# ─── Market Regime Dashboard ──────────────────────────────────────────────────

@api_market_bp.route('/screener/market-regime')
@login_required
def get_market_regime_overview():
    """Get aggregate market regime overview — market phase, fear/greed, distribution."""
    from app.services.market_regime_monitor import MarketRegimeMonitor
    monitor = MarketRegimeMonitor()
    result = monitor.get_market_overview(
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


@api_market_bp.route('/screener/market-regime/assets')
@login_required
def get_market_regime_coins():
    """Get per-asset regime data for table view."""
    from app.services.market_regime_monitor import MarketRegimeMonitor
    monitor = MarketRegimeMonitor()
    result = monitor.get_coin_regimes(
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


# ─── DCA Optimizer ────────────────────────────────────────────────────────────

@api_market_bp.route('/screener/dca')
@api_market_bp.route('/dca-optimizer')
@login_required
def get_dca_optimizer():
    """Get ranked DCA optimizer candidates — best assets for dollar-cost averaging."""
    from app.services.dca_optimizer import DCAOptimizer
    optimizer = DCAOptimizer()
    result = optimizer.get_candidates(
        tier_filter=request.args.get('tier', 'all'),
        min_score=request.args.get('min_score', 0, type=float),
        sort_by=request.args.get('sort', 'score'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


# ─── Correlation Scanner ──────────────────────────────────────────────────────

@api_market_bp.route('/screener/correlation/groups')
@login_required
def get_correlation_groups():
    """Get correlated asset groups."""
    from app.services.correlation_scanner import CorrelationScanner
    scanner = CorrelationScanner()
    result = scanner.get_correlation_groups(
        min_group_size=request.args.get('min_size', 2, type=int),
        asset_type=request.args.get('asset_type', 'all'),
        limit=min(request.args.get('limit', 20, type=int), 50),
        page=request.args.get('page', 1, type=int),
    )
    return jsonify(result)


@api_market_bp.route('/screener/correlation/asset/<int:asset_id>')
@login_required
def get_coin_correlations(asset_id):
    """Get top correlated assets for a specific asset."""
    from app.services.correlation_scanner import CorrelationScanner
    scanner = CorrelationScanner()
    result = scanner.get_coin_correlations(
        asset_id=asset_id,
        limit=min(request.args.get('limit', 10, type=int), 50),
    )
    return jsonify(result)


@api_market_bp.route('/screener/correlation/diversification')
@login_required
def get_diversification_score():
    """Get diversification score for a set of assets.

    Query params: ids=1,2,3,4  (comma-separated asset IDs)
    """
    from app.services.correlation_scanner import CorrelationScanner
    raw_ids = request.args.get('ids', '')
    asset_ids = []
    for part in raw_ids.split(','):
        part = part.strip()
        if part.isdigit():
            asset_ids.append(int(part))
    if len(asset_ids) < 2:
        return jsonify({'error': 'Need at least 2 asset IDs (ids=1,2,3)'}), 400
    scanner = CorrelationScanner()
    result = scanner.get_diversification_score(asset_ids=asset_ids)
    return jsonify(result)


# ─── Wave 3: Advanced Insights Endpoints ─────────────────────────────────────

@api_market_bp.route('/screener/signal-performance')
@login_required
def get_signal_performance():
    """Aggregate signal performance stats — win rate, accuracy, breakdowns."""
    from app.services.signal_performance_tracker import SignalPerformanceTracker
    tracker = SignalPerformanceTracker()
    result = tracker.get_performance_summary(
        asset_type=request.args.get('asset_type', 'all'),
        strategy=request.args.get('strategy', 'all'),
        signal_type=request.args.get('signal_type', 'all'),
        timeframe_days=request.args.get('days', 30, type=int),
    )
    return jsonify(result)


@api_market_bp.route('/screener/signal-performance/history')
@login_required
def get_signal_history():
    """Paginated history of evaluated signals with outcomes."""
    from app.services.signal_performance_tracker import SignalPerformanceTracker
    tracker = SignalPerformanceTracker()
    result = tracker.get_signal_history(
        sort_by=request.args.get('sort', 'date'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
        outcome=request.args.get('outcome', 'all'),
        strategy=request.args.get('strategy', 'all'),
    )
    return jsonify(result)


@api_market_bp.route('/sector-rotation')
@login_required
def get_sector_rotation():
    """Sector rotation overview — sector scores & rotation signals."""
    from app.services.sector_rotation_map import SectorRotationService
    svc = SectorRotationService()
    result = svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
    )
    return jsonify(result)


@api_market_bp.route('/screener/sector-rotation/assets')
@login_required
def get_sector_coins():
    """Coins within a specific sector."""
    from app.services.sector_rotation_map import SectorRotationService
    sector = request.args.get('sector', '')
    if not sector:
        return jsonify({'error': 'sector param required'}), 400
    svc = SectorRotationService()
    result = svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 200, type=int), 500),
    )
    # Filter to requested sector if scan_all returned successfully
    if result.get('status') == 'success' and result.get('data', {}).get('sectors'):
        sectors = result['data']['sectors']
        matched = [s for s in sectors if s.get('name', '').lower() == sector.lower()]
        if matched:
            result['data'] = matched[0]
        else:
            result = {'status': 'error', 'message': f'Sector "{sector}" not found'}
    return jsonify(result)


@api_market_bp.route('/screener/momentum-heatmap')
@login_required
def get_momentum_heatmap():
    """Momentum heatmap — price change visualization grid."""
    from app.services.momentum_heatmap import MomentumHeatmap
    heatmap = MomentumHeatmap()
    result = heatmap.get_heatmap(
        group_by=request.args.get('group', 'asset_type'),
        timeframe=request.args.get('timeframe', '24h'),
        sort_by=request.args.get('sort', 'change'),
        limit=min(request.args.get('limit', 200, type=int), 500),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


@api_market_bp.route('/screener/anomalies')
@login_required
def get_anomalies():
    """Anomaly detector — unusual price/volume patterns."""
    from app.services.anomaly_detector import AnomalyDetector
    detector = AnomalyDetector()
    result = detector.get_anomalies(
        severity=request.args.get('severity', 'all'),
        anomaly_type=request.args.get('type', 'all'),
        sort_by=request.args.get('sort', 'severity'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


@api_market_bp.route('/screener/risk-calculator')
@login_required
def get_risk_overview():
    """Risk overview — position sizing for all active-signal assets."""
    from app.services.risk_calculator import RiskCalculator
    calc = RiskCalculator()
    result = calc.get_risk_overview(
        sort_by=request.args.get('sort', 'rr_ratio'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
        capital_idr=request.args.get('capital', 1_000_000, type=float),
        risk_pct=request.args.get('risk', 2.0, type=float),
    )
    return jsonify(result)


@api_market_bp.route('/screener/risk-calculator/<asset_id>')
@login_required
def calculate_position(asset_id):
    """Calculate position sizing for a specific asset."""
    from app.services.risk_calculator import RiskCalculator
    calc = RiskCalculator()
    result = calc.calculate_position(
        asset_id=asset_id,
        capital_idr=request.args.get('capital', 1_000_000, type=float),
        risk_pct=request.args.get('risk', 2.0, type=float),
    )
    return jsonify(result)


@api_market_bp.route('/screener/leaderboard')
@login_required
def get_leaderboard():
    """Leaderboard — top performing assets ranked by composite score."""
    from app.services.leaderboard import Leaderboard
    lb = Leaderboard()
    result = lb.get_leaderboard(
        category=request.args.get('category', 'overall'),
        timeframe=request.args.get('timeframe', '7d'),
        sort_by=request.args.get('sort', 'rank'),
        limit=min(request.args.get('limit', 20, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        asset_type=request.args.get('asset_type', 'all'),
    )
    return jsonify(result)


# ─── Extended Stock Data Endpoints ───────────────────────────────────────────

_EXTENDED_DATA_TYPES = {
    'financials', 'quarterly_financials', 'balance_sheet',
    'quarterly_balance_sheet', 'cash_flow', 'quarterly_cash_flow',
    'ttm_cash_flow',
    'earnings', 'earnings_dates', 'calendar',
    'dividends', 'splits',
    'recommendations', 'recommendations_summary',
    'upgrades_downgrades', 'analyst_price_targets',
    'major_holders', 'institutional_holders', 'mutualfund_holders',
    'insider_transactions', 'insider_purchases', 'insider_roster_holders',
    'news',
    'options_dates', 'options_chain',
    'growth_estimates', 'revenue_estimate', 'earnings_estimate',
    'eps_trend', 'eps_revisions',
    'sustainability',
    'sec_filings',
}


@api_market_bp.route('/asset/<asset_id>/extended/<data_type>')
@login_required
def get_extended_data(asset_id, data_type):
    """Return extended stock data for a asset.

    GET /api/v1/market/asset/<asset_id>/extended/<data_type>
    Supported data_type values: financials, quarterly_financials,
    balance_sheet, quarterly_balance_sheet, cash_flow, quarterly_cash_flow,
    earnings, earnings_dates, calendar, dividends, splits, recommendations,
    recommendations_summary, upgrades_downgrades, analyst_price_targets,
    major_holders, institutional_holders, mutualfund_holders,
    insider_transactions, insider_purchases, insider_roster_holders, news.
    """
    if data_type not in _EXTENDED_DATA_TYPES:
        return jsonify({'error': f'Invalid data_type: {data_type}'}), 400

    from app.models.asset_extended_data import AssetExtendedData
    record = AssetExtendedData.query.filter_by(
        asset_id=asset_id, data_type=data_type
    ).first()

    if not record:
        return jsonify({'data': None, 'fetched_at': None})

    return jsonify({
        'data': record.data_json,
        'fetched_at': record.fetched_at.isoformat() if record.fetched_at else None,
    })


@api_market_bp.route('/asset/<asset_id>/extended')
@login_required
def get_all_extended_data(asset_id):
    """Return all available extended data types for a asset.

    GET /api/v1/market/asset/<asset_id>/extended
    Returns a summary dict: { data_type: { fetched_at, has_data } }
    """
    from app.models.asset_extended_data import AssetExtendedData
    records = AssetExtendedData.query.filter_by(asset_id=asset_id).all()

    result = {}
    for r in records:
        result[r.data_type] = {
            'fetched_at': r.fetched_at.isoformat() if r.fetched_at else None,
            'has_data': r.data_json is not None,
        }
    return jsonify(result)


# ─── Sector / Industry / Screener Yahoo Endpoints ────────────────────────────

@api_market_bp.route('/sectors/<sector_key>')
@login_required
def get_sector_data(sector_key):
    """Get Yahoo Finance sector data.

    GET /api/v1/market/sectors/<sector_key>
    sector_key examples: technology, healthcare, financial-services, etc.
    """
    from app.services.data_sync.router import DataRouter
    router = DataRouter()
    # Use yahoo provider directly (sector data is market-level, not asset-specific)
    provider = None
    for p_id, p in router.get_all_providers().items():
        if hasattr(p, 'get_sector_data'):
            provider = p
            break

    if not provider:
        return jsonify({'error': 'Yahoo provider not available'}), 503

    try:
        # Check cache first (valid for 6 hours)
        from app.models.market_cache import MarketCache
        from sqlalchemy.dialects.mysql import insert as mysql_insert
        cache_key = f'sector:{sector_key}'
        cached = MarketCache.query.filter_by(cache_key=cache_key).first()
        if cached and cached.fetched_at:
            age_hours = (datetime.utcnow() - cached.fetched_at).total_seconds() / 3600
            if age_hours < 6 and cached.data_json:
                return jsonify(cached.data_json)

        data = provider.get_sector_data(sector_key)
        if not data:
            return jsonify({'error': f'No data found for sector: {sector_key}'}), 404

        # Persist to MarketCache (UPSERT)
        try:
            stmt = mysql_insert(MarketCache).values(
                cache_key=cache_key, data_json=data, fetched_at=datetime.utcnow())
            stmt = stmt.on_duplicate_key_update(
                data_json=stmt.inserted.data_json, fetched_at=stmt.inserted.fetched_at)
            db.session.execute(stmt)
            db.session.commit()
        except Exception as cache_err:
            logger.warning(f'Failed to cache sector data: {cache_err}')

        return jsonify(data)
    except Exception as e:
        return jsonify({'error': str(e)[:200]}), 500


@api_market_bp.route('/industries/<industry_key>')
@login_required
def get_industry_data(industry_key):
    """Get Yahoo Finance industry data.

    GET /api/v1/market/industries/<industry_key>
    industry_key examples: semiconductors, software-application, etc.
    """
    from app.services.data_sync.router import DataRouter
    router = DataRouter()
    provider = None
    for p_id, p in router.get_all_providers().items():
        if hasattr(p, 'get_industry_data'):
            provider = p
            break

    if not provider:
        return jsonify({'error': 'Yahoo provider not available'}), 503

    try:
        # Check cache first (valid for 6 hours)
        from app.models.market_cache import MarketCache
        from sqlalchemy.dialects.mysql import insert as mysql_insert
        cache_key = f'industry:{industry_key}'
        cached = MarketCache.query.filter_by(cache_key=cache_key).first()
        if cached and cached.fetched_at:
            age_hours = (datetime.utcnow() - cached.fetched_at).total_seconds() / 3600
            if age_hours < 6 and cached.data_json:
                return jsonify(cached.data_json)

        data = provider.get_industry_data(industry_key)
        if not data:
            return jsonify({'error': f'No data found for industry: {industry_key}'}), 404

        # Persist to MarketCache (UPSERT)
        try:
            stmt = mysql_insert(MarketCache).values(
                cache_key=cache_key, data_json=data, fetched_at=datetime.utcnow())
            stmt = stmt.on_duplicate_key_update(
                data_json=stmt.inserted.data_json, fetched_at=stmt.inserted.fetched_at)
            db.session.execute(stmt)
            db.session.commit()
        except Exception as cache_err:
            logger.warning(f'Failed to cache industry data: {cache_err}')

        return jsonify(data)
    except Exception as e:
        return jsonify({'error': str(e)[:200]}), 500


@api_market_bp.route('/screeners/yahoo')
@login_required
def get_yahoo_screeners():
    """Get Yahoo Finance predefined screeners.

    GET /api/v1/market/screeners/yahoo
    Returns list of predefined screener queries (day_gainers, day_losers,
    most_actives, undervalued_growth_stocks, etc.).
    """
    from app.services.data_sync.router import DataRouter
    router = DataRouter()
    provider = None
    for p_id, p in router.get_all_providers().items():
        if hasattr(p, 'get_predefined_screeners'):
            provider = p
            break

    if not provider:
        return jsonify({'error': 'Yahoo provider not available'}), 503

    try:
        data = provider.get_predefined_screeners()
        # Only return keys — the query objects are not JSON-serializable
        screener_keys = {k: {'sortField': v.get('sortField', ''), 'sortType': v.get('sortType', '')}
                         for k, v in data.items()} if isinstance(data, dict) else {}
        return jsonify({'screeners': screener_keys})
    except Exception as e:
        return jsonify({'error': str(e)[:200]}), 500


@api_market_bp.route('/screeners/yahoo/run', methods=['POST'])
@login_required
def run_yahoo_screener():
    """Run a Yahoo Finance predefined screener and return results.

    POST /api/v1/market/screeners/yahoo/run
    Body: {"screener_key": "day_gainers", "count": 25}
    """
    import yfinance as yf

    body = request.get_json(silent=True) or {}
    key = body.get('screener_key', 'day_gainers')
    count = min(int(body.get('count', 25)), 100)
    force_refresh = body.get('force', False)

    # Check cache first (valid for 30 minutes)
    if not force_refresh:
        try:
            from app.models.market_cache import MarketCache
            cache_key = f'screener:{key}:{count}'
            cached = MarketCache.query.filter_by(cache_key=cache_key).first()
            if cached and cached.fetched_at:
                age_min = (datetime.utcnow() - cached.fetched_at).total_seconds() / 60
                if age_min < 30 and cached.data_json:
                    return jsonify(cached.data_json)
        except Exception:
            pass

    try:
        # Validate key exists in predefined screeners
        predefined = {}
        if hasattr(yf, 'PREDEFINED_SCREENER_QUERIES'):
            predefined = yf.PREDEFINED_SCREENER_QUERIES

        if key not in predefined:
            return jsonify({'error': f'Unknown screener: {key}',
                            'available': list(predefined.keys())}), 400

        # yf.screen() accepts the key string directly for predefined screeners
        result = yf.screen(key, count=count)

        # Convert result to serializable format
        if result is None:
            return jsonify({'results': [], 'total': 0})

        quotes = []
        if isinstance(result, dict):
            quotes = result.get('quotes', [])
        elif isinstance(result, list):
            quotes = result

        # Clean quotes: remove NaN/non-serializable values
        import math
        cleaned = []
        for q in quotes[:count]:
            clean_q = {}
            for k, v in q.items():
                if isinstance(v, float) and (math.isnan(v) or math.isinf(v)):
                    clean_q[k] = None
                else:
                    clean_q[k] = v
            cleaned.append(clean_q)

        response_data = {
            'screener': key,
            'results': cleaned,
            'total': result.get('total', len(cleaned)) if isinstance(result, dict) else len(cleaned),
        }

        # Cache screener results to DB (valid for 30 minutes)
        try:
            from app.models.market_cache import MarketCache
            from sqlalchemy.dialects.mysql import insert as mysql_insert
            cache_key = f'screener:{key}:{count}'
            stmt = mysql_insert(MarketCache).values(
                cache_key=cache_key, data_json=response_data,
                fetched_at=datetime.utcnow())
            stmt = stmt.on_duplicate_key_update(
                data_json=stmt.inserted.data_json,
                fetched_at=stmt.inserted.fetched_at)
            db.session.execute(stmt)
            db.session.commit()
        except Exception as cache_err:
            logger.warning(f'Failed to cache screener results: {cache_err}')

        return jsonify(response_data)
    except Exception as e:
        logger.error(f'Yahoo screener run failed: {e}')
        return jsonify({'error': str(e)[:200]}), 500


@api_market_bp.route('/batch-ohlcv', methods=['POST'])
@login_required
def batch_download_ohlcv():
    """Batch download OHLCV for multiple assets using yf.download().

    POST /api/v1/market/batch-ohlcv
    Body: {"asset_ids": ["NASDAQ.AAPL", "NASDAQ.MSFT"], "timeframe": "1D", "period": "1y"}
    """
    from app.services.data_sync.router import DataRouter

    body = request.get_json(silent=True) or {}
    asset_ids = body.get('asset_ids', [])
    timeframe = body.get('timeframe', '1D')
    period = body.get('period', '')

    if not asset_ids or len(asset_ids) > 50:
        return jsonify({'error': 'Provide 1-50 asset_ids'}), 400

    router = DataRouter()
    provider = None
    for p_id, p in router.get_all_providers().items():
        if hasattr(p, 'batch_download_ohlcv'):
            provider = p
            break

    if not provider:
        return jsonify({'error': 'Yahoo provider not available'}), 503

    try:
        data = provider.batch_download_ohlcv(asset_ids, timeframe, period)
        summary = {cid: len(candles) for cid, candles in data.items()}
        return jsonify({'summary': summary, 'total_coins': len(data)})
    except Exception as e:
        return jsonify({'error': str(e)[:200]}), 500


# ─── Market Status & Calendars ────────────────────────────────────────────────

@api_market_bp.route('/market-status/<market_key>')
@login_required
def get_market_status(market_key):
    """Get market status (open/close hours).

    GET /api/v1/market/market-status/<market_key>
    market_key examples: us_market, gb_market, de_market, jp_market
    Cached in MarketCache for 30 minutes.
    """
    from datetime import datetime, timedelta
    from app.models.market_cache import MarketCache

    cache_key = f'market_status:{market_key}'
    cached = MarketCache.query.filter_by(cache_key=cache_key).first()
    if cached and cached.fetched_at > datetime.utcnow() - timedelta(minutes=30):
        return jsonify(cached.data_json)

    from app.services.data_sync.router import DataRouter
    router = DataRouter()
    provider = None
    for p_id, p in router.get_all_providers().items():
        if hasattr(p, 'get_market_status'):
            provider = p
            break

    if not provider:
        return jsonify({'error': 'Yahoo provider not available'}), 503

    data = provider.get_market_status(market_key)
    if not data:
        return jsonify({'error': f'Market "{market_key}" not found or unavailable'}), 404

    # Cache result
    from sqlalchemy.dialects.mysql import insert as mysql_insert
    stmt = mysql_insert(MarketCache).values(
        cache_key=cache_key, data_json=data, fetched_at=datetime.utcnow()
    ).on_duplicate_key_update(data_json=data, fetched_at=datetime.utcnow())
    db.session.execute(stmt)
    db.session.commit()

    return jsonify(data)


@api_market_bp.route('/market-calendars')
@login_required
def get_market_calendars():
    """Get market-wide calendars (earnings, IPO, splits, economic events).

    GET /api/v1/market/market-calendars
    Cached in MarketCache for 1 hour.
    """
    from datetime import datetime, timedelta
    from app.models.market_cache import MarketCache

    cache_key = 'market_calendars'
    cached = MarketCache.query.filter_by(cache_key=cache_key).first()
    if cached and cached.fetched_at > datetime.utcnow() - timedelta(hours=1):
        return jsonify(cached.data_json)

    from app.services.data_sync.router import DataRouter
    router = DataRouter()
    provider = None
    for p_id, p in router.get_all_providers().items():
        if hasattr(p, 'get_market_calendars'):
            provider = p
            break

    if not provider:
        return jsonify({'error': 'Yahoo provider not available'}), 503

    data = provider.get_market_calendars()
    if not data:
        return jsonify({'error': 'No calendar data available'}), 404

    # Cache result
    from sqlalchemy.dialects.mysql import insert as mysql_insert
    stmt = mysql_insert(MarketCache).values(
        cache_key=cache_key, data_json=data, fetched_at=datetime.utcnow()
    ).on_duplicate_key_update(data_json=data, fetched_at=datetime.utcnow())
    db.session.execute(stmt)
    db.session.commit()

    return jsonify(data)


# ------------------------------------------------------------------
# Outcome Tracking & Signal Performance Endpoints
# ------------------------------------------------------------------

@api_market_bp.route('/screener/signals/update-outcomes', methods=['POST'])
@admin_required
def update_signal_outcomes():
    """Evaluate pending signals against actual price movements.

    Checks SL/TP hits and current-price comparison for signals that
    have matured beyond their evaluation delay window.

    Body (optional JSON):
        limit: max signals to evaluate (default 200)

    Returns summary: {evaluated, win, loss, breakeven, skipped, errors}
    """
    from app.services.outcome_tracker import OutcomeTracker

    data = request.get_json(silent=True) or {}
    limit = data.get('limit', 200)

    tracker = OutcomeTracker()
    results = tracker.update_all_pending(limit=limit)

    return jsonify({
        'ok': True,
        'results': results,
    })


@api_market_bp.route('/screener/signals/performance', methods=['GET'])
@login_required
def signal_performance_stats():
    """Get historical signal performance stats.

    Query params:
        asset_id: filter by asset (optional)
        days: lookback period in days (default 90)

    Returns: winrate, avg_win, avg_loss, profit_factor, kelly_fraction, etc.
    """
    from app.services.outcome_tracker import OutcomeTracker

    asset_id = request.args.get('asset_id')
    days = int(request.args.get('days', 90))

    tracker = OutcomeTracker()
    stats = tracker.get_performance_stats(asset_id=asset_id, days=days)

    return jsonify({
        'ok': True,
        'stats': stats,
    })


# ── Feature 1: Telegram Alert Bot ────────────────────────────────────────────

@api_market_bp.route('/alerts/telegram/config', methods=['GET'])
@login_required
def get_telegram_config():
    """Get Telegram bot configuration."""
    from app.models.alert_config import AlertConfig
    cfg = AlertConfig.get_all()
    return jsonify({
        'bot_token': cfg.get('telegram_bot_token', ''),
        'chat_id': cfg.get('telegram_chat_id', ''),
        'enabled': cfg.get('telegram_enabled', 'false') == 'true',
        'alert_buy_signals': cfg.get('alert_buy_signals', 'true') == 'true',
        'alert_sell_signals': cfg.get('alert_sell_signals', 'true') == 'true',
        'alert_price_alerts': cfg.get('alert_price_alerts', 'true') == 'true',
        'alert_scan_complete': cfg.get('alert_scan_complete', 'true') == 'true',
    })


@api_market_bp.route('/alerts/telegram/config', methods=['POST'])
@login_required
def update_telegram_config():
    """Update Telegram bot configuration."""
    from app.models.alert_config import AlertConfig
    data = request.get_json(force=True)
    for key in ('telegram_bot_token', 'telegram_chat_id', 'telegram_enabled',
                'alert_buy_signals', 'alert_sell_signals', 'alert_price_alerts',
                'alert_scan_complete'):
        if key in data:
            val = data[key]
            if isinstance(val, bool):
                val = 'true' if val else 'false'
            AlertConfig.set(key, val)
    return jsonify({'ok': True})


@api_market_bp.route('/alerts/telegram/test', methods=['POST'])
@login_required
def test_telegram():
    """Send a test message via Telegram."""
    from app.services.telegram_alert import TelegramAlertService
    svc = TelegramAlertService()
    result = svc.test_connection()
    return jsonify(result)


@api_market_bp.route('/alerts/telegram/logs')
@login_required
def get_telegram_logs():
    """Get recent alert logs."""
    from app.services.telegram_alert import TelegramAlertService
    svc = TelegramAlertService()
    limit = request.args.get('limit', 50, type=int)
    alert_type = request.args.get('type')
    return jsonify({
        'logs': svc.get_alert_logs(limit, alert_type),
        'stats': svc.get_stats(),
    })


# ── Feature 2: Performance Attribution ────────────────────────────────────────

@api_market_bp.route('/performance/attribution')
@login_required
def get_performance_attribution():
    """Get performance attribution breakdown.

    Query params:
        period: Lookback days (default: 30)
        group_by: 'strategy', 'signal_type', 'timeframe', 'confidence', 'safety', 'asset'
        asset_type: 'all', 'crypto', 'stock', 'stock_us'
    """
    from app.services.performance_attribution import PerformanceAttributionService
    svc = PerformanceAttributionService()
    result = svc.get_attribution(
        period_days=request.args.get('period', 30, type=int),
        asset_type=request.args.get('asset_type', 'all'),
        group_by=request.args.get('group_by', 'strategy'),
    )
    return jsonify(result)


# ── Feature 3: Auto-Scan Scheduler ───────────────────────────────────────────

@api_market_bp.route('/scan/schedules')
@login_required
def get_scan_schedules():
    """Get all scan schedules."""
    from app.services.scan_scheduler import ScanSchedulerService
    svc = ScanSchedulerService()
    return jsonify({'schedules': svc.get_schedules(), 'scan_types': svc.SCAN_TYPES})


@api_market_bp.route('/scan/schedules', methods=['POST'])
@login_required
def create_scan_schedule():
    """Create a new scan schedule."""
    from app.services.scan_scheduler import ScanSchedulerService
    data = request.get_json(force=True)
    svc = ScanSchedulerService()
    sid = svc.create_schedule(
        name=data.get('name', 'New Scan'),
        scan_type=data.get('scan_type', 'full_scan'),
        asset_type=data.get('asset_type', 'crypto'),
        interval_hours=data.get('interval_hours', 4),
        send_telegram=data.get('send_telegram', True),
    )
    return jsonify({'ok': True, 'schedule_id': sid})


@api_market_bp.route('/scan/schedules/<int:sid>', methods=['PUT'])
@login_required
def update_scan_schedule(sid):
    """Update a scan schedule."""
    from app.services.scan_scheduler import ScanSchedulerService
    data = request.get_json(force=True)
    svc = ScanSchedulerService()
    ok = svc.update_schedule(sid, **data)
    return jsonify({'ok': ok})


@api_market_bp.route('/scan/schedules/<int:sid>', methods=['DELETE'])
@login_required
def delete_scan_schedule(sid):
    """Delete a scan schedule."""
    from app.services.scan_scheduler import ScanSchedulerService
    svc = ScanSchedulerService()
    ok = svc.delete_schedule(sid)
    return jsonify({'ok': ok})


@api_market_bp.route('/scan/run/<int:sid>', methods=['POST'])
@login_required
def run_scan_now(sid):
    """Manually trigger a scan."""
    from app.services.scan_scheduler import ScanSchedulerService
    svc = ScanSchedulerService()
    result = svc.run_scan(sid)
    return jsonify(result)


@api_market_bp.route('/scan/run-due', methods=['POST'])
@login_required
def run_due_scans():
    """Run all overdue scans."""
    from app.services.scan_scheduler import ScanSchedulerService
    svc = ScanSchedulerService()
    results = svc.run_due_scans()
    return jsonify({'ok': True, 'results': results})


@api_market_bp.route('/scan/init-defaults', methods=['POST'])
@login_required
def init_default_schedules():
    """Initialize default scan schedules."""
    from app.services.scan_scheduler import ScanSchedulerService
    svc = ScanSchedulerService()
    created = svc.get_default_schedules()
    return jsonify({'ok': True, 'created': created})


# ── Feature 4: Price Alerts ──────────────────────────────────────────────────

@api_market_bp.route('/alerts/price')
@login_required
def get_price_alerts():
    """Get user's price alerts."""
    from app.services.price_alert_service import PriceAlertService
    from app.helpers.auth import get_current_user_id
    svc = PriceAlertService()
    is_active = request.args.get('active')
    if is_active is not None:
        is_active = is_active.lower() == 'true'
    return jsonify({
        'alerts': svc.get_alerts(
            user_id=get_current_user_id(),
            is_active=is_active,
            asset_id=request.args.get('asset_id'),
            limit=request.args.get('limit', 50, type=int),
        ),
        'stats': svc.get_stats(get_current_user_id()),
    })


@api_market_bp.route('/alerts/price', methods=['POST'])
@login_required
def create_price_alert():
    """Create a new price alert."""
    from app.services.price_alert_service import PriceAlertService
    from app.helpers.auth import get_current_user_id
    data = request.get_json(force=True)
    svc = PriceAlertService()
    aid = svc.create_alert(
        user_id=get_current_user_id(),
        asset_id=data['asset_id'],
        alert_type=data['alert_type'],
        target_price=data.get('target_price'),
        pct_threshold=data.get('pct_threshold'),
        note=data.get('note'),
    )
    return jsonify({'ok': True, 'alert_id': aid})


@api_market_bp.route('/alerts/price/<int:aid>', methods=['DELETE'])
@login_required
def delete_price_alert(aid):
    """Delete a price alert."""
    from app.services.price_alert_service import PriceAlertService
    from app.helpers.auth import get_current_user_id
    svc = PriceAlertService()
    ok = svc.delete_alert(aid, get_current_user_id())
    return jsonify({'ok': ok})


@api_market_bp.route('/alerts/price/<int:aid>/toggle', methods=['POST'])
@login_required
def toggle_price_alert(aid):
    """Toggle a price alert active state."""
    from app.services.price_alert_service import PriceAlertService
    from app.helpers.auth import get_current_user_id
    svc = PriceAlertService()
    new_state = svc.toggle_alert(aid, get_current_user_id())
    return jsonify({'ok': new_state is not None, 'is_active': new_state})


@api_market_bp.route('/alerts/price/check', methods=['POST'])
@login_required
def check_price_alerts():
    """Manually check all active price alerts."""
    from app.services.price_alert_service import PriceAlertService
    svc = PriceAlertService()
    triggered = svc.check_alerts()
    return jsonify({'ok': True, 'triggered': triggered, 'count': len(triggered)})


# ── Feature 5: Portfolio Optimizer ───────────────────────────────────────────

@api_market_bp.route('/portfolio/optimize')
@login_required
def optimize_portfolio():
    """Get optimal portfolio allocation.

    Query params:
        risk: 'conservative', 'balanced', 'aggressive' (default: 'balanced')
        capital: Capital IDR (default: 10000000)
        max_assets: Maximum assets (default: 10)
        asset_type: 'all', 'crypto', 'stock', 'stock_us'
    """
    from app.services.portfolio_optimizer import PortfolioOptimizer
    optimizer = PortfolioOptimizer()
    result = optimizer.optimize(
        asset_type=request.args.get('asset_type', 'all'),
        risk_profile=request.args.get('risk', 'balanced'),
        capital_idr=request.args.get('capital', 10_000_000, type=float),
        max_assets=request.args.get('max_assets', 10, type=int),
    )
    return jsonify(result)


# ══════════════════════════════════════════════════════════════════════════════
# WAVE 5: Profit Maximizer Pro Tools
# ══════════════════════════════════════════════════════════════════════════════

# ── Feature 6: Adaptive SL/TP Optimizer ──────────────────────────────────────

@api_market_bp.route('/sltp/optimal')
@login_required
def get_optimal_sltp():
    """Get adaptive SL/TP levels for a asset."""
    from app.services.adaptive_sltp import AdaptiveSLTPService
    svc = AdaptiveSLTPService()
    asset_id = request.args.get('asset_id')
    if not asset_id:
        return jsonify({'error': 'asset_id required'}), 400
    result = svc.get_optimal_levels(
        asset_id=asset_id,
        entry_price=request.args.get('entry_price', type=float),
        direction=request.args.get('direction', 'long'),
    )
    return jsonify(result)


@api_market_bp.route('/sltp/batch')
@login_required
def get_batch_sltp():
    """Get adaptive SL/TP for top buy signals."""
    from app.services.adaptive_sltp import AdaptiveSLTPService
    svc = AdaptiveSLTPService()
    return jsonify(svc.get_batch_levels(
        asset_type=request.args.get('asset_type', 'crypto'),
        direction=request.args.get('direction', 'long'),
        limit=request.args.get('limit', 20, type=int),
    ))


@api_market_bp.route('/sltp/compare')
@login_required
def compare_sltp():
    """Compare fixed vs adaptive SL/TP."""
    from app.services.adaptive_sltp import AdaptiveSLTPService
    svc = AdaptiveSLTPService()
    asset_id = request.args.get('asset_id')
    if not asset_id:
        return jsonify({'error': 'asset_id required'}), 400
    return jsonify(svc.get_comparison(
        asset_id=asset_id,
        entry_price=request.args.get('entry_price', type=float),
    ))


# ── Feature 7: Trade Journal ────────────────────────────────────────────────

@api_market_bp.route('/journal/trades')
@login_required
def get_journal_trades():
    """Get user's trade journal entries."""
    from app.services.trade_journal import TradeJournalService
    from app.helpers.auth import get_current_user_id
    svc = TradeJournalService()
    is_open = request.args.get('open')
    if is_open is not None:
        is_open = is_open.lower() == 'true'
    return jsonify({
        'trades': svc.get_trades(
            user_id=get_current_user_id(),
            is_open=is_open,
            asset_id=request.args.get('asset_id'),
            setup=request.args.get('setup'),
            limit=request.args.get('limit', 50, type=int),
        ),
        'stats': svc.get_stats(get_current_user_id()),
    })


@api_market_bp.route('/journal/trades', methods=['POST'])
@login_required
def create_journal_trade():
    """Log a new trade entry."""
    from app.services.trade_journal import TradeJournalService
    from app.helpers.auth import get_current_user_id
    data = request.get_json(force=True)
    svc = TradeJournalService()
    from datetime import datetime
    tid = svc.create_trade(
        user_id=get_current_user_id(),
        asset_id=data['asset_id'],
        entry_price=data['entry_price'],
        entry_date=data.get('entry_date', datetime.utcnow().isoformat()),
        direction=data.get('direction', 'long'),
        signal_id=data.get('signal_id'),
        position_size=data.get('position_size'),
        trade_setup=data.get('trade_setup'),
        market_regime=data.get('market_regime'),
        notes=data.get('notes'),
        emotion_tag=data.get('emotion_tag'),
    )
    return jsonify({'ok': tid is not None, 'trade_id': tid})


@api_market_bp.route('/journal/trades/<int:tid>/close', methods=['POST'])
@login_required
def close_journal_trade(tid):
    """Close a trade with exit details."""
    from app.services.trade_journal import TradeJournalService
    from app.helpers.auth import get_current_user_id
    data = request.get_json(force=True)
    svc = TradeJournalService()
    from datetime import datetime
    result = svc.close_trade(
        trade_id=tid,
        user_id=get_current_user_id(),
        exit_price=data['exit_price'],
        exit_date=data.get('exit_date', datetime.utcnow().isoformat()),
        exit_reason=data.get('exit_reason', 'manual'),
        fees=data.get('fees', 0),
        notes=data.get('notes'),
    )
    return jsonify({'ok': result is not None, 'result': result})


@api_market_bp.route('/journal/trades/<int:tid>', methods=['DELETE'])
@login_required
def delete_journal_trade(tid):
    """Delete a trade from journal."""
    from app.services.trade_journal import TradeJournalService
    from app.helpers.auth import get_current_user_id
    svc = TradeJournalService()
    ok = svc.delete_trade(tid, get_current_user_id())
    return jsonify({'ok': ok})


@api_market_bp.route('/journal/analytics')
@login_required
def get_journal_analytics():
    """Get trade journal analytics."""
    from app.services.trade_journal import TradeJournalService
    from app.helpers.auth import get_current_user_id
    svc = TradeJournalService()
    return jsonify(svc.get_analytics(
        user_id=get_current_user_id(),
        period_days=request.args.get('period', 30, type=int),
        setup=request.args.get('setup'),
    ))


# ── Feature 8: Smart Entry Timing ───────────────────────────────────────────

@api_market_bp.route('/entry/opportunities')
@login_required
def get_entry_opportunities():
    """Get smart entry opportunities for active signals."""
    from app.services.smart_entry import SmartEntryService
    svc = SmartEntryService()
    return jsonify(svc.get_entry_opportunities(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=request.args.get('limit', 20, type=int),
    ))


@api_market_bp.route('/entry/analysis')
@login_required
def get_entry_analysis():
    """Detailed entry analysis for a asset."""
    from app.services.smart_entry import SmartEntryService
    svc = SmartEntryService()
    asset_id = request.args.get('asset_id')
    if not asset_id:
        return jsonify({'error': 'asset_id required'}), 400
    return jsonify(svc.get_entry_analysis(
        asset_id=asset_id,
        signal_id=request.args.get('signal_id', type=int),
    ))


# ── Feature 9: Liquidity Analysis ───────────────────────────────────────────

@api_market_bp.route('/liquidity/scores')
@login_required
def get_liquidity_scores():
    """Get liquidity scores for all assets."""
    from app.services.liquidity_analyzer import LiquidityAnalyzer
    svc = LiquidityAnalyzer()
    return jsonify(svc.get_liquidity_scores(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=request.args.get('limit', 30, type=int),
    ))


@api_market_bp.route('/liquidity/execution-risk')
@login_required
def check_execution_risk():
    """Check execution risk for a trade."""
    from app.services.liquidity_analyzer import LiquidityAnalyzer
    svc = LiquidityAnalyzer()
    asset_id = request.args.get('asset_id')
    if not asset_id:
        return jsonify({'error': 'asset_id required'}), 400
    return jsonify(svc.check_execution_risk(
        asset_id=asset_id,
        position_size_usd=request.args.get('position', 1000, type=float),
    ))


@api_market_bp.route('/liquidity/volume-confirm')
@login_required
def volume_confirmation():
    """Check volume confirmation for recent signals."""
    from app.services.liquidity_analyzer import LiquidityAnalyzer
    svc = LiquidityAnalyzer()
    return jsonify(svc.volume_confirmation(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=request.args.get('limit', 20, type=int),
    ))


# ── Feature 10: ML Health Monitor ───────────────────────────────────────────

@api_market_bp.route('/ml/health')
@login_required
def get_ml_health():
    """Get ML model health report."""
    from app.services.ml_health_monitor import MLHealthMonitor
    svc = MLHealthMonitor()
    return jsonify(svc.get_health_report(
        asset_type=request.args.get('asset_type', 'crypto'),
        period_days=request.args.get('period', 30, type=int),
    ))


# ══════════════════════════════════════════════════════════════════════════════
# WAVE 6: Edge Maximizer Tools
# ══════════════════════════════════════════════════════════════════════════════

# ── Feature 11: Sessions & Hourly Analysis ───────────────────────────────────

@api_market_bp.route('/sessions/analysis')
@login_required
def get_session_analysis():
    """Get win rate by hour, day, and trading session."""
    from app.services.session_analyzer import SessionAnalyzer
    svc = SessionAnalyzer()
    return jsonify(svc.get_session_analysis(
        asset_type=request.args.get('asset_type', 'crypto'),
        period_days=request.args.get('period', 90, type=int),
        source=request.args.get('source', 'signals'),
    ))


# ── Feature 12: Funding Rate Analysis ───────────────────────────────────────

@api_market_bp.route('/funding/analysis')
@login_required
def get_funding_analysis():
    """Get funding rate analysis for contrarian signals."""
    from app.services.funding_rate import FundingRateAnalyzer
    svc = FundingRateAnalyzer()
    return jsonify(svc.get_funding_analysis(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=request.args.get('limit', 30, type=int),
    ))


# ── Feature 13: Pain Point Filter ───────────────────────────────────────────

@api_market_bp.route('/pain-points')
@login_required
def get_pain_points():
    """Get toxic pattern analysis."""
    from app.services.pain_point_filter import PainPointFilter
    svc = PainPointFilter()
    return jsonify(svc.get_pain_points(
        asset_type=request.args.get('asset_type', 'crypto'),
        period_days=request.args.get('period', 90, type=int),
        source=request.args.get('source', 'signals'),
    ))


# ── Feature 14: Confluence Scoring ──────────────────────────────────────────

@api_market_bp.route('/confluence/scores')
@login_required
def get_confluence_scores():
    """Get multi-factor confluence scores for active signals."""
    from app.services.confluence_scorer import ConfluenceScorer
    svc = ConfluenceScorer()
    return jsonify(svc.get_confluence_report(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=request.args.get('limit', 30, type=int),
    ))


# ── Feature 15: Kelly Criterion Position Sizer ──────────────────────────────

@api_market_bp.route('/kelly/report')
@login_required
def get_kelly_report():
    """Get Kelly Criterion position sizing report."""
    from app.services.kelly_calculator import KellyCalculator
    svc = KellyCalculator()
    return jsonify(svc.get_kelly_report(
        asset_type=request.args.get('asset_type', 'crypto'),
        period_days=request.args.get('period', 90, type=int),
        capital=request.args.get('capital', 10_000_000, type=float),
        risk_profile=request.args.get('risk', 'moderate'),
    ))


# ── Wave 7: Quantum Edge Tools ────────────────────────────────────────────────

# ── Feature 16: Volatility Regime Position Sizer ──────────────────────────────

@api_market_bp.route('/regime-sizing')
@login_required
def get_regime_sizing():
    """Get volatility regime-aware position sizing."""
    from app.services.volatility_regime_sizer import VolatilityRegimeSizer
    svc = VolatilityRegimeSizer()
    return jsonify(svc.get_regime_sizing(
        asset_type=request.args.get('asset_type', 'crypto'),
        capital=request.args.get('capital', 10_000_000, type=float),
        risk_pct=request.args.get('risk_pct', 2.0, type=float),
        period_days=request.args.get('period', 90, type=int),
    ))


# ── Feature 17: Mean Reversion Cycle Predictor ────────────────────────────────

@api_market_bp.route('/cycle-predictions')
@login_required
def get_cycle_predictions():
    """Get mean reversion cycle completion predictions."""
    from app.services.cycle_predictor import CyclePredictor
    svc = CyclePredictor()
    return jsonify(svc.get_cycle_predictions(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 18: Price Action Microstructure Exploiter ─────────────────────────

@api_market_bp.route('/microstructure')
@login_required
def get_microstructure():
    """Get price action microstructure signals."""
    from app.services.microstructure_exploiter import MicrostructureExploiter
    svc = MicrostructureExploiter()
    return jsonify(svc.get_microstructure_signals(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 19: Drawdown Recovery Sequencer ───────────────────────────────────

@api_market_bp.route('/drawdown-recovery')
@login_required
def get_drawdown_recovery():
    """Get ranked drawdown recovery opportunities."""
    from app.services.drawdown_recovery import DrawdownRecoverySequencer
    svc = DrawdownRecoverySequencer()
    return jsonify(svc.get_recovery_opportunities(
        asset_type=request.args.get('asset_type', 'crypto'),
        min_drawdown=request.args.get('min_dd', 20, type=float),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 20: Multi-Asset Contagion Detector ────────────────────────────────

@api_market_bp.route('/contagion')
@login_required
def get_contagion():
    """Get multi-asset contagion analysis."""
    from app.services.contagion_detector import ContagionDetector
    svc = ContagionDetector()
    return jsonify(svc.get_contagion_analysis(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Wave 8: Alpha Shield Tools ────────────────────────────────────────────────

# ── Feature 21: Win Rate Collapse Detector ────────────────────────────────────

@api_market_bp.route('/winrate-collapse')
@login_required
def get_winrate_collapse():
    """Get win rate collapse detection report."""
    from app.services.winrate_collapse import WinRateCollapseDetector
    svc = WinRateCollapseDetector()
    return jsonify(svc.get_collapse_report(
        asset_type=request.args.get('asset_type', 'crypto'),
        baseline_days=request.args.get('baseline', 90, type=int),
    ))


# ── Feature 22: Correlation Pair Trader ───────────────────────────────────────

@api_market_bp.route('/pair-trading')
@login_required
def get_pair_trading():
    """Get correlation pair trading opportunities."""
    from app.services.pair_trading_scanner import PairTradingScannerService
    svc = PairTradingScannerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 23: Forced Liquidation Predictor ──────────────────────────────────

@api_market_bp.route('/liquidation-predictor')
@login_required
def get_liquidation_predictor():
    """Get forced liquidation cascade predictions."""
    from app.services.liquidation_predictor import LiquidationPredictor
    svc = LiquidationPredictor()
    return jsonify(svc.get_liquidation_analysis(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 24: Intraday Capital Allocator ────────────────────────────────────

@api_market_bp.route('/capital-allocation')
@login_required
def get_capital_allocation():
    """Get optimal intraday capital allocation plan."""
    from app.services.capital_allocator import IntradayCapitalAllocator
    svc = IntradayCapitalAllocator()
    return jsonify(svc.get_allocation_plan(
        asset_type=request.args.get('asset_type', 'crypto'),
        capital=request.args.get('capital', 10_000_000, type=float),
        max_positions=request.args.get('max_pos', 10, type=int),
        period_days=request.args.get('period', 30, type=int),
    ))


# ── Feature 25: Post-Exit Re-entry Filter ────────────────────────────────────

@api_market_bp.route('/reentry-filter')
@login_required
def get_reentry_filter():
    """Get post-exit re-entry filter analysis."""
    from app.services.reentry_filter import ReentryFilter
    svc = ReentryFilter()
    return jsonify(svc.get_reentry_analysis(
        asset_type=request.args.get('asset_type', 'crypto'),
        cooldown_hours=request.args.get('cooldown', 4, type=int),
        period_days=request.args.get('period', 60, type=int),
    ))


# ── Wave 9: Signal Harvester Tools ────────────────────────────────────────────

# ── Feature 26: Earnings Event Screener ───────────────────────────────────────

@api_market_bp.route('/earnings-screener')
@login_required
def get_earnings_screener():
    """Get earnings-driven trade setups."""
    from app.services.earnings_screener import EarningsScreener
    svc = EarningsScreener()
    return jsonify(svc.get_earnings_report(
        asset_type=request.args.get('asset_type', 'stock'),
        days_ahead=request.args.get('days', 30, type=int),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 27: Insider Accumulation Detector ─────────────────────────────────

@api_market_bp.route('/insider-detector')
@login_required
def get_insider_detector():
    """Get insider accumulation/distribution signals."""
    from app.services.insider_detector import InsiderDetector
    svc = InsiderDetector()
    return jsonify(svc.get_insider_analysis(
        asset_type=request.args.get('asset_type', 'stock'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 28: Dividend Recapture Optimizer ──────────────────────────────────

@api_market_bp.route('/dividend-optimizer')
@login_required
def get_dividend_optimizer():
    """Get dividend capture & DRIP optimization report."""
    from app.services.dividend_optimizer import DividendOptimizer
    svc = DividendOptimizer()
    return jsonify(svc.get_dividend_report(
        asset_type=request.args.get('asset_type', 'stock'),
        capital=request.args.get('capital', 10_000_000, type=float),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 29: Event Arbitrage Correlator ────────────────────────────────────

@api_market_bp.route('/event-arbitrage')
@login_required
def get_event_arbitrage():
    """Get cross-asset event arbitrage opportunities."""
    from app.services.event_arbitrage import EventArbitrageCorrelator
    svc = EventArbitrageCorrelator()
    return jsonify(svc.get_arbitrage_opportunities(
        asset_type=request.args.get('asset_type', 'all'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 30: Recovery Path Predictor ───────────────────────────────────────

@api_market_bp.route('/recovery-predictor')
@login_required
def get_recovery_predictor():
    """Get ML-based recovery path predictions."""
    from app.services.recovery_predictor import RecoveryPathPredictor
    svc = RecoveryPathPredictor()
    return jsonify(svc.get_recovery_predictions(
        asset_type=request.args.get('asset_type', 'crypto'),
        min_drawdown=request.args.get('min_dd', 15, type=float),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 31: Analyst Conviction Detector ──────────────────────────────────

@api_market_bp.route('/analyst-conviction')
@login_required
def get_analyst_conviction():
    """Get analyst consensus signals & conviction scores."""
    from app.services.analyst_conviction import AnalystConvictionDetector
    svc = AnalystConvictionDetector()
    return jsonify(svc.get_analyst_conviction(
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 32: Options Flow Imbalance Monitor ───────────────────────────────

@api_market_bp.route('/options-flow')
@login_required
def get_options_flow():
    """Get options flow analysis with put/call ratios & unusual activity."""
    from app.services.options_flow import OptionsFlowMonitor
    svc = OptionsFlowMonitor()
    return jsonify(svc.get_options_flow(
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 33: Fundamental Momentum Scorer ──────────────────────────────────

@api_market_bp.route('/fundamental-momentum')
@login_required
def get_fundamental_momentum():
    """Get fundamental quality & momentum scores from financial data."""
    from app.services.fundamental_momentum import FundamentalMomentumScorer
    svc = FundamentalMomentumScorer()
    return jsonify(svc.get_fundamental_momentum(
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 34: Social Momentum Spike Detector ──────────────────────────────

@api_market_bp.route('/social-momentum')
@login_required
def get_social_momentum():
    """Get social media & community momentum spikes."""
    from app.services.social_momentum import SocialMomentumDetector
    svc = SocialMomentumDetector()
    return jsonify(svc.get_social_momentum(
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 35: Whale Accumulation Tracker ───────────────────────────────────

@api_market_bp.route('/whale-tracker')
@login_required
def get_whale_tracker():
    """Get whale accumulation & institutional ownership signals."""
    from app.services.whale_transaction_tracker import WhaleTransactionTrackerService
    svc = WhaleTransactionTrackerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 36: Signal Orchestrator (Master Aggregator) ──────────────────────

@api_market_bp.route('/signal-orchestrator')
@login_required
def get_signal_orchestrator():
    """Get master aggregated signals from all tools."""
    from app.services.signal_orchestrator import SignalOrchestrator
    svc = SignalOrchestrator()
    return jsonify(svc.get_master_signals(
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 37: Backtest Engine ──────────────────────────────────────────────

@api_market_bp.route('/backtest')
@login_required
def get_backtest():
    """Run a backtest on historical data with selected strategy."""
    from app.services.backtest_engine import BacktestEngine
    svc = BacktestEngine()
    return jsonify(svc.run_backtest(
        strategy=request.args.get('strategy', 'score_based'),
        lookback_days=request.args.get('days', 90, type=int),
        limit=min(request.args.get('limit', 30, type=int), 50),
    ))


# ── Feature 38: Supply/Demand Zone Mapper ────────────────────────────────────

@api_market_bp.route('/supply-demand')
@login_required
def get_supply_demand():
    """Detect institutional supply & demand zones with order blocks."""
    from app.services.supply_demand_zones import SupplyDemandZoneService
    svc = SupplyDemandZoneService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 39: News Sentiment Analyzer ──────────────────────────────────────

@api_market_bp.route('/news-sentiment')
@login_required
def get_news_sentiment():
    """Get news-based sentiment analysis per asset."""
    from app.services.news_sentiment import NewsSentimentAnalyzer
    svc = NewsSentimentAnalyzer()
    return jsonify(svc.get_news_sentiment(
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 40: Risk Parity Allocator ────────────────────────────────────────

@api_market_bp.route('/risk-parity')
@login_required
def get_risk_parity():
    """Get risk parity portfolio allocation."""
    from app.services.risk_parity_allocator import RiskParityAllocatorService
    svc = RiskParityAllocatorService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
    ))


# ═══════════════════════════════════════════════════════════════════════════════
# WAVE 12 — PROFIT MAXIMIZER (Features 41-50)
# ═══════════════════════════════════════════════════════════════════════════════


# ── Feature 41: Multi-Timeframe Confluence ───────────────────────────────────

@api_market_bp.route('/multi-timeframe')
@login_required
def get_multi_timeframe():
    """Scan multi-timeframe confluence signals across all assets."""
    from app.services.multi_timeframe import MultiTimeframeService
    svc = MultiTimeframeService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 42: Volume Profile Analyzer ──────────────────────────────────────

@api_market_bp.route('/volume-profile')
@login_required
def get_volume_profile():
    """Analyze volume profile with POC, Value Area, HVN/LVN."""
    from app.services.volume_profile import VolumeProfileService
    svc = VolumeProfileService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(
            symbol=symbol,
            asset_type=request.args.get('asset_type', 'crypto'),
            num_levels=request.args.get('levels', 50, type=int),
        ))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 43: AI Chart Pattern Recognition ─────────────────────────────────

@api_market_bp.route('/chart-patterns')
@login_required
def get_chart_patterns():
    """Detect chart patterns (H&S, triangles, wedges, flags, etc)."""
    from app.services.chart_patterns import ChartPatternService
    svc = ChartPatternService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 44: Smart Money Concepts (SMC) ───────────────────────────────────

@api_market_bp.route('/smart-money')
@login_required
def get_smart_money():
    """Analyze Smart Money Concepts: OB, FVG, liquidity sweeps, BOS/CHoCH."""
    from app.services.smart_money import SmartMoneyService
    svc = SmartMoneyService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 45: Mean Reversion Scanner ───────────────────────────────────────

@api_market_bp.route('/mean-reversion')
@login_required
def get_mean_reversion():
    """Scan for mean-reversion opportunities using z-score, Bollinger, Hurst."""
    from app.services.mean_reversion import MeanReversionService
    svc = MeanReversionService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(
            symbol=symbol,
            asset_type=request.args.get('asset_type', 'crypto'),
            lookback=request.args.get('lookback', 20, type=int),
        ))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 46: Monte Carlo Risk Simulator ───────────────────────────────────

@api_market_bp.route('/monte-carlo')
@login_required
def get_monte_carlo():
    """Run Monte Carlo simulation for VaR, CVaR, probability of ruin."""
    from app.services.monte_carlo import MonteCarloService
    svc = MonteCarloService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.simulate(
            symbols=[symbol],
            asset_type=request.args.get('asset_type', 'crypto'),
            num_simulations=min(request.args.get('simulations', 5000, type=int), 10000),
            days=request.args.get('days', 30, type=int),
        ))
    return jsonify(svc.portfolio_simulation(
        asset_type=request.args.get('asset_type', 'crypto'),
        days=request.args.get('days', 30, type=int),
        num_simulations=min(request.args.get('simulations', 5000, type=int), 10000),
    ))


# ── Feature 47: Gap Scanner ─────────────────────────────────────────────────

@api_market_bp.route('/gap-scanner')
@login_required
def get_gap_scanner():
    """Detect and analyze price gaps with fill probability."""
    from app.services.gap_scanner import GapScannerService
    svc = GapScannerService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 48: Auto-Rebalancer ──────────────────────────────────────────────

@api_market_bp.route('/auto-rebalancer')
@login_required
def get_auto_rebalancer():
    """Calculate portfolio rebalancing suggestions."""
    from app.services.auto_rebalancer import AutoRebalancerService
    svc = AutoRebalancerService()
    return jsonify(svc.analyze(
        asset_type=request.args.get('asset_type', 'crypto'),
        strategy=request.args.get('strategy', 'equal_weight'),
    ))


# ── Feature 49: Volatility Squeeze Detector ──────────────────────────────────

@api_market_bp.route('/volatility-squeeze')
@login_required
def get_volatility_squeeze():
    """Detect Bollinger-Keltner squeeze for pending breakouts."""
    from app.services.volatility_squeeze import VolatilitySqueezeService
    svc = VolatilitySqueezeService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 50: Cross-Asset Correlation Dashboard ────────────────────────────

@api_market_bp.route('/cross-asset')
@login_required
def get_cross_asset():
    """Cross-asset correlation analysis with divergence detection."""
    from app.services.cross_asset import CrossAssetService
    svc = CrossAssetService()
    return jsonify(svc.analyze(
        asset_type=request.args.get('asset_type', 'crypto'),
    ))


# ═══════════════════════════════════════════════════════════════════════════════
# WAVE 13 — ADVANCED TRADING EDGE (Features 51-60)
# ═══════════════════════════════════════════════════════════════════════════════


# ── Feature 51: Harmonic Pattern Scanner ──────────────────────────────────────

@api_market_bp.route('/harmonic-patterns')
@login_required
def get_harmonic_patterns():
    """Detect harmonic patterns (Gartley, Butterfly, Bat, Crab, Shark)."""
    from app.services.harmonic_patterns import HarmonicPatternService
    svc = HarmonicPatternService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 52: Ichimoku Cloud Scanner ────────────────────────────────────────

@api_market_bp.route('/ichimoku')
@login_required
def get_ichimoku():
    """Full Ichimoku Cloud analysis with signal scoring."""
    from app.services.ichimoku_scanner import IchimokuScannerService
    svc = IchimokuScannerService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 53: Wyckoff Phase Analyzer ────────────────────────────────────────

@api_market_bp.route('/wyckoff')
@login_required
def get_wyckoff():
    """Identify Wyckoff market phases and events."""
    from app.services.wyckoff_analyzer import WyckoffAnalyzerService
    svc = WyckoffAnalyzerService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 54: VWAP Analysis Suite ───────────────────────────────────────────

@api_market_bp.route('/vwap')
@login_required
def get_vwap():
    """VWAP analysis with deviation bands and anchored VWAP."""
    from app.services.vwap_analysis import VWAPAnalysisService
    svc = VWAPAnalysisService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 55: Fibonacci Cluster Zones ───────────────────────────────────────

@api_market_bp.route('/fibonacci-clusters')
@login_required
def get_fibonacci_clusters():
    """Multi-Fibonacci confluence zones with cluster analysis."""
    from app.services.fibonacci_clusters import FibonacciClusterService
    svc = FibonacciClusterService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 56: Market Maker Detector ─────────────────────────────────────────

@api_market_bp.route('/market-maker')
@login_required
def get_market_maker():
    """Detect market maker activity patterns."""
    from app.services.market_maker_detector import MarketMakerDetectorService
    svc = MarketMakerDetectorService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 57: Fear & Greed Index ────────────────────────────────────────────

@api_market_bp.route('/fear-greed')
@login_required
def get_fear_greed():
    """Composite Fear & Greed Index from multiple market indicators."""
    from app.services.fear_greed_index import FearGreedIndexService
    svc = FearGreedIndexService()
    return jsonify(svc.calculate(
        asset_type=request.args.get('asset_type', 'crypto'),
    ))


# ── Feature 58: ML Ensemble Predictor ─────────────────────────────────────────

@api_market_bp.route('/ml-ensemble')
@login_required
def get_ml_ensemble():
    """ML ensemble predictions (RF + GBM + Ridge)."""
    from app.services.ml_ensemble import MLEnsembleService
    svc = MLEnsembleService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.predict(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 59: Regime Strategy Selector ──────────────────────────────────────

@api_market_bp.route('/regime-strategy')
@login_required
def get_regime_strategy():
    """Auto-select best trading strategy per market regime."""
    from app.services.regime_strategy import RegimeStrategyService
    svc = RegimeStrategyService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 60: On-Chain Analytics ────────────────────────────────────────────

@api_market_bp.route('/on-chain')
@login_required
def get_on_chain():
    """On-chain analytics with exchange flow, whale detection, MVRV."""
    from app.services.on_chain_analytics import OnChainAnalyticsService
    svc = OnChainAnalyticsService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ═══════════════════════════════════════════════════════════════════════════════
# WAVE 14 — EXECUTION EXCELLENCE (Features 61-70)
# ═══════════════════════════════════════════════════════════════════════════════


# ── Feature 61: Divergence Scanner ────────────────────────────────────────────

@api_market_bp.route('/divergence-scanner')
@login_required
def get_divergence_scanner():
    """Detect RSI / MACD / OBV / Volume divergences."""
    from app.services.divergence_scanner import DivergenceScannerService
    svc = DivergenceScannerService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 62: Order Flow Imbalance ─────────────────────────────────────────

@api_market_bp.route('/order-flow')
@login_required
def get_order_flow():
    """Analyze order flow imbalance — buy/sell pressure & delta volume."""
    from app.services.order_flow_imbalance import OrderFlowImbalanceService
    svc = OrderFlowImbalanceService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 63: Adaptive Grid Bot ────────────────────────────────────────────

@api_market_bp.route('/grid-bot')
@login_required
def get_grid_bot():
    """Calculate optimal grid-bot parameters per asset volatility."""
    from app.services.adaptive_grid_bot import AdaptiveGridBotService
    svc = AdaptiveGridBotService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(
            symbol=symbol,
            asset_type=request.args.get('asset_type', 'crypto'),
            capital=request.args.get('capital', 10_000_000, type=float),
            grids=request.args.get('grids', 10, type=int),
        ))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 64: Swing Failure Pattern ────────────────────────────────────────

@api_market_bp.route('/swing-failure')
@login_required
def get_swing_failure():
    """Detect Swing Failure Patterns (SFP) — failed breakout/breakdown."""
    from app.services.swing_failure_pattern import SwingFailurePatternService
    svc = SwingFailurePatternService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 65: Market Profile TPO ───────────────────────────────────────────

@api_market_bp.route('/market-profile')
@login_required
def get_market_profile():
    """Market Profile (TPO) — POC, Value Area, Single Prints."""
    from app.services.market_profile_tpo import MarketProfileTPOService
    svc = MarketProfileTPOService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 66: Trailing Stop Engine ─────────────────────────────────────────

@api_market_bp.route('/trailing-stop')
@login_required
def get_trailing_stop():
    """Compare 6 trailing-stop strategies for optimal exit."""
    from app.services.trailing_stop_engine import TrailingStopEngineService
    svc = TrailingStopEngineService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 67: Compound Growth Planner ──────────────────────────────────────

@api_market_bp.route('/compound-growth')
@login_required
def get_compound_growth():
    """Portfolio compound growth projections & milestone planner."""
    from app.services.compound_growth_planner import CompoundGrowthPlannerService
    svc = CompoundGrowthPlannerService()
    return jsonify(svc.calculate(
        initial_capital=request.args.get('capital', 10_000_000, type=float),
        monthly_return_pct=request.args.get('monthly_return', 5.0, type=float),
        years=request.args.get('years', 5, type=int),
        reinvest_pct=request.args.get('reinvest', 100.0, type=float),
        monthly_addition=request.args.get('monthly_add', 0, type=float),
        asset_type=request.args.get('asset_type', 'crypto'),
    ))


# ── Feature 68: Profit Factor Analyzer ───────────────────────────────────────

@api_market_bp.route('/profit-factor')
@login_required
def get_profit_factor():
    """Analyze profit factor & expectancy per strategy/signal type."""
    from app.services.profit_factor_analyzer import ProfitFactorAnalyzerService
    svc = ProfitFactorAnalyzerService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 69: Trade Timing Index ───────────────────────────────────────────

@api_market_bp.route('/trade-timing')
@login_required
def get_trade_timing():
    """Optimal trade timing — hour, session & day-of-week patterns."""
    from app.services.trade_timing_index import TradeTimingIndexService
    svc = TradeTimingIndexService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 70: AI Portfolio Coach ───────────────────────────────────────────

@api_market_bp.route('/ai-coach')
@login_required
def get_ai_coach():
    """AI-powered portfolio coaching — personalized recommendations."""
    from app.services.ai_portfolio_coach import AIPortfolioCoachService
    svc = AIPortfolioCoachService()
    return jsonify(svc.get_recommendations(
        portfolio_symbols=request.args.get('symbols', '').split(',') if request.args.get('symbols') else None,
        risk_profile=request.args.get('risk_profile', 'moderate'),
        asset_type=request.args.get('asset_type', 'crypto'),
    ))


# ═══════════════════════════════════════════════════════════════════════════════
# WAVE 15 — ALPHA GENERATION & MASTERY (New Features Only)
# ═══════════════════════════════════════════════════════════════════════════════


# ── Feature 71: Elliott Wave Counter ─────────────────────────────────────────

@api_market_bp.route('/elliott-wave')
@login_required
def get_elliott_wave():
    """Automated Elliott Wave counting — impulse & corrective waves."""
    from app.services.elliott_wave_counter import ElliottWaveCounterService
    svc = ElliottWaveCounterService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 76: Liquidation Heatmap ──────────────────────────────────────────

@api_market_bp.route('/liquidation-heatmap')
@login_required
def get_liquidation_heatmap():
    """Estimate leveraged liquidation zones & squeeze probability."""
    from app.services.liquidation_heatmap import LiquidationHeatmapService
    svc = LiquidationHeatmapService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 78: Social Sentiment Index ───────────────────────────────────────

@api_market_bp.route('/social-sentiment')
@login_required
def get_social_sentiment():
    """Behavioral sentiment proxy — fear/greed from price action patterns."""
    from app.services.social_sentiment_index import SocialSentimentService
    svc = SocialSentimentService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ── Feature 79: Strategy Backtester ──────────────────────────────────────────

@api_market_bp.route('/strategy-backtester')
@login_required
def get_strategy_backtester():
    """Full strategy backtester — 5 strategies with equity curves & stats."""
    from app.services.strategy_backtester import StrategyBacktesterService
    svc = StrategyBacktesterService()
    symbol = request.args.get('symbol')
    if symbol:
        return jsonify(svc.analyze(symbol=symbol, asset_type=request.args.get('asset_type', 'crypto')))
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
    ))


# ═══════════════════════════════════════════════════════════════════════════════
# WAVE 16 — MASTERY & INTELLIGENCE SUITE (Features 81-90)
# ═══════════════════════════════════════════════════════════════════════════════


# ── Feature 81: Portfolio Stress Test ─────────────────────────────────────────

@api_market_bp.route('/stress-test')
@login_required
def get_stress_test():
    """Portfolio Stress Test — crash scenario analysis & vulnerability scoring."""
    from app.services.portfolio_stress_test import PortfolioStressTestService
    svc = PortfolioStressTestService()
    return jsonify(svc.run_stress_test(
        scenario=request.args.get('scenario', 'btc_crash'),
        severity=request.args.get('severity', 'moderate'),
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        sort_by=request.args.get('sort', 'risk_desc'),
    ))


# ── Feature 82: Market Breadth Scanner ────────────────────────────────────────

@api_market_bp.route('/market-breadth')
@login_required
def get_market_breadth():
    """Market Breadth Scanner — advance/decline, market health metrics."""
    from app.services.market_breadth import MarketBreadthService
    svc = MarketBreadthService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
    ))


# ── Feature 83: Relative Strength Ranker ──────────────────────────────────────

@api_market_bp.route('/relative-strength')
@login_required
def get_relative_strength():
    """Relative Strength Ranker — RS vs BTC/market benchmark."""
    from app.services.relative_strength import RelativeStrengthService
    svc = RelativeStrengthService()
    return jsonify(svc.scan_all(
        benchmark=request.args.get('benchmark', 'btc'),
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        sort_by=request.args.get('sort', 'rs_desc'),
        filter_outperform=request.args.get('filter', 'all'),
    ))


# ── Feature 84: Exit Strategy Planner ─────────────────────────────────────────

@api_market_bp.route('/exit-planner')
@login_required
def get_exit_planner():
    """Exit Strategy Planner — staged profit-taking plans."""
    from app.services.exit_planner import ExitPlannerService
    svc = ExitPlannerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        sort_by=request.args.get('sort', 'rr_desc'),
        min_rr=request.args.get('min_rr', 0, type=float),
    ))


# ── Feature 85: Accumulation Zone Detector ────────────────────────────────────

@api_market_bp.route('/accumulation-zone')
@login_required
def get_accumulation_zone():
    """Accumulation Zone Detector — smart entry zone identification."""
    from app.services.accumulation_zone import AccumulationZoneService
    svc = AccumulationZoneService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        search_q=request.args.get('q', '').strip(),
        sort_by=request.args.get('sort', 'score_desc'),
        zone_filter=request.args.get('zone', 'all'),
    ))


# ── Feature 86: Volatility Surface Analyzer ───────────────────────────────────

@api_market_bp.route('/volatility-surface')
@login_required
def get_volatility_surface():
    """Volatility Surface Analyzer — vol structure across assets."""
    from app.services.volatility_surface import VolatilitySurfaceService
    svc = VolatilitySurfaceService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'vol_desc'),
        regime_filter=request.args.get('vol_regime'),
    ))


# ── Feature 87: Alpha Decay Monitor ──────────────────────────────────────────

@api_market_bp.route('/alpha-decay')
@login_required
def get_alpha_decay():
    """Alpha Decay Monitor — signal freshness & staleness tracking."""
    from app.services.alpha_decay import AlphaDecayService
    svc = AlphaDecayService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'stalest'),
        alert_filter=request.args.get('alert_level'),
    ))


# ── Feature 88: Whale Flow Aggregator ────────────────────────────────────────

@api_market_bp.route('/whale-flow')
@login_required
def get_whale_flow():
    """Whale Flow Aggregator — aggregate whale movement patterns."""
    from app.services.whale_flow import WhaleFlowService
    svc = WhaleFlowService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'flow_strength'),
        flow_filter=request.args.get('flow'),
    ))


# ── Feature 89: Trend Strength Index ─────────────────────────────────────────

@api_market_bp.route('/trend-strength')
@login_required
def get_trend_strength():
    """Trend Strength Index — composite trend scoring system."""
    from app.services.trend_strength import TrendStrengthService
    svc = TrendStrengthService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'trend_desc'),
        trend_filter=request.args.get('trend'),
    ))


# ── Feature 90: Risk-Reward Scanner ──────────────────────────────────────────

@api_market_bp.route('/risk-reward')
@login_required
def get_risk_reward():
    """Risk-Reward Scanner — optimal R:R entry point detection."""
    from app.services.risk_reward_scanner import RiskRewardScannerService
    svc = RiskRewardScannerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        min_rr=request.args.get('min_rr', 2.0, type=float),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'rr_desc'),
    ))


# ═══════════════════════════════════════════════════════════════════════════════
# WAVE 17 — ULTIMATE PROFIT ENGINE (Features 91-100)
# ═══════════════════════════════════════════════════════════════════════════════


# ── Feature 91: Sniper Entry ─────────────────────────────────────────────────

@api_market_bp.route('/sniper-entry')
@login_required
def get_sniper_entry():
    """Sniper Entry — precision entry combining ALL signal sources."""
    from app.services.sniper_entry import SniperEntryService
    svc = SniperEntryService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'fire_score_desc'),
        min_fire_score=request.args.get('min_fire', 0, type=int),
    ))


# ── Feature 92: Reversal Radar ───────────────────────────────────────────────

@api_market_bp.route('/reversal-radar')
@login_required
def get_reversal_radar():
    """Reversal Radar — early trend reversal detection."""
    from app.services.reversal_radar import ReversalRadarService
    svc = ReversalRadarService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'reversal_score_desc'),
        reversal_type=request.args.get('reversal_type', 'all'),
    ))


# ── Feature 93: Money Flow Tracker ───────────────────────────────────────────

@api_market_bp.route('/money-flow')
@login_required
def get_money_flow():
    """Money Flow Tracker — capital flow between sectors & assets."""
    from app.services.money_flow_tracker import MoneyFlowTrackerService
    svc = MoneyFlowTrackerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'flow_score_desc'),
        flow_direction=request.args.get('flow', 'all'),
    ))


# ── Feature 94: Catalyst Calendar ────────────────────────────────────────────

@api_market_bp.route('/catalyst-calendar')
@login_required
def get_catalyst_calendar():
    """Catalyst Calendar — event-driven profit opportunities."""
    from app.services.catalyst_calendar import CatalystCalendarService
    svc = CatalystCalendarService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'impact_desc'),
        catalyst_type=request.args.get('catalyst_type', 'all'),
    ))


# ── Feature 95: Smart Money Copier ───────────────────────────────────────────

@api_market_bp.route('/smart-money-copy')
@login_required
def get_smart_money_copy():
    """Smart Money Copier — mirror whale/institutional patterns."""
    from app.services.smart_money_copier import SmartMoneyCopierService
    svc = SmartMoneyCopierService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'smart_score_desc'),
        pattern=request.args.get('pattern', 'all'),
    ))


# ── Feature 96: Cascade Profiter ─────────────────────────────────────────────

@api_market_bp.route('/cascade-profiter')
@login_required
def get_cascade_profiter():
    """Cascade Profiter — profit from liquidation cascades."""
    from app.services.cascade_profiter import CascadeProfilerService
    svc = CascadeProfilerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'cascade_risk_desc'),
        alert_level=request.args.get('alert_level', 'all'),
    ))


# ── Feature 97: Correlation Breakout ─────────────────────────────────────────

@api_market_bp.route('/correlation-breakout')
@login_required
def get_correlation_breakout():
    """Correlation Breakout — assets breaking from BTC/market correlation."""
    from app.services.correlation_breakout import CorrelationBreakoutService
    svc = CorrelationBreakoutService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'breakout_score_desc'),
        breakout_type=request.args.get('breakout_type', 'all'),
    ))


# ── Feature 98: Momentum Rotator ─────────────────────────────────────────────

@api_market_bp.route('/momentum-rotator')
@login_required
def get_momentum_rotator():
    """Momentum Rotator — auto-rotate to hottest sectors/assets."""
    from app.services.momentum_rotator import MomentumRotatorService
    svc = MomentumRotatorService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'rotation_score_desc'),
        timeframe=request.args.get('timeframe', '7d'),
    ))


# ── Feature 99: Profit Maximizer ─────────────────────────────────────────────

@api_market_bp.route('/profit-maximizer')
@login_required
def get_profit_maximizer():
    """Profit Maximizer — staged profit-taking + trailing optimization."""
    from app.services.profit_maximizer import ProfitMaximizerService
    svc = ProfitMaximizerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'profit_potential_desc'),
        status_filter=request.args.get('status', 'all'),
    ))


# ── Feature 100: AI Trade Architect ──────────────────────────────────────────

@api_market_bp.route('/ai-trade-architect')
@login_required
def get_ai_trade_architect():
    """AI Trade Architect — AI-generated complete trade plans."""
    from app.services.ai_trade_architect import AITradeArchitectService
    svc = AITradeArchitectService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'conviction_desc'),
        min_conviction=request.args.get('min_conviction', 0, type=int),
    ))


# ══════════════════════════════════════════════════════════════════════════════
#  WAVE 18 — Wealth Acceleration Suite (Features 102-111)
# ══════════════════════════════════════════════════════════════════════════════


# ── Feature 102: Wealth Dashboard ────────────────────────────────────────────

@api_market_bp.route('/screener/wealth-dashboard')
@login_required
def get_wealth_dashboard():
    """Wealth Dashboard — aggregate portfolio intelligence & market overview."""
    from app.services.wealth_dashboard import WealthDashboardService
    svc = WealthDashboardService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'score_desc'),
        timeframe=request.args.get('timeframe', '7d'),
    ))


# ── Feature 103: Income Stream Mapper ────────────────────────────────────────

@api_market_bp.route('/screener/income-streams')
@login_required
def get_income_streams():
    """Income Stream Mapper — find assets for passive income strategies."""
    from app.services.income_stream_mapper import IncomeStreamMapperService
    svc = IncomeStreamMapperService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'income_score_desc'),
        strategy=request.args.get('strategy', 'all'),
    ))


# ── Feature 104: Whale Momentum Decoder ──────────────────────────────────────

@api_market_bp.route('/screener/whale-momentum')
@login_required
def get_whale_momentum():
    """Whale Momentum Decoder — detect institutional/whale activity patterns."""
    from app.services.whale_momentum_decoder import WhaleMomentumDecoderService
    svc = WhaleMomentumDecoderService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'whale_score_desc'),
        activity_type=request.args.get('activity', 'all'),
    ))


# ── Feature 105: Momentum Cascade Finder ─────────────────────────────────────

@api_market_bp.route('/screener/momentum-cascade')
@login_required
def get_momentum_cascade():
    """Momentum Cascade — find multi-timeframe momentum alignment."""
    from app.services.momentum_cascade_finder import MomentumCascadeFinderService
    svc = MomentumCascadeFinderService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'cascade_score_desc'),
        alignment=request.args.get('alignment', 'all'),
    ))


# ── Feature 106: Position Architect ──────────────────────────────────────────

@api_market_bp.route('/screener/position-architect')
@login_required
def get_position_architect():
    """Position Architect — generate complete position plans with entry/exit zones."""
    from app.services.position_architect import PositionArchitectService
    svc = PositionArchitectService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'opportunity_desc'),
        signal_status=request.args.get('status', 'all'),
    ))


# ── Feature 107: Market Cycle Navigator ──────────────────────────────────────

@api_market_bp.route('/screener/market-cycle')
@login_required
def get_market_cycle():
    """Market Cycle Navigator — macro cycle phase analysis & rotation."""
    from app.services.market_cycle_navigator import MarketCycleNavigatorService
    svc = MarketCycleNavigatorService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'cycle_score_desc'),
        phase=request.args.get('phase', 'all'),
    ))


# ── Feature 108: Crypto Season Detector ──────────────────────────────────────

@api_market_bp.route('/screener/crypto-season')
@login_required
def get_crypto_season():
    """Crypto Season Detector — BTC vs altcoin vs sector rotation detection."""
    from app.services.crypto_season_detector import CryptoSeasonDetectorService
    svc = CryptoSeasonDetectorService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'season_score_desc'),
        season=request.args.get('season', 'all'),
    ))


# ── Feature 109: Alpha Generator ─────────────────────────────────────────────

@api_market_bp.route('/screener/alpha-generator')
@login_required
def get_alpha_generator():
    """Alpha Generator — identify highest alpha (outperformance) potential."""
    from app.services.alpha_generator import AlphaGeneratorService
    svc = AlphaGeneratorService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'alpha_desc'),
        min_alpha=request.args.get('min_alpha', 0, type=float),
    ))


# ── Feature 110: Entry Zone Mapper ───────────────────────────────────────────

@api_market_bp.route('/screener/entry-zones')
@login_required
def get_entry_zones():
    """Entry Zone Mapper — map precise buy/sell/wait zones for every asset."""
    from app.services.entry_zone_mapper import EntryZoneMapperService
    svc = EntryZoneMapperService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'entry_score_desc'),
        zone=request.args.get('zone', 'all'),
    ))


# ── Feature 111: Profit Lock System ──────────────────────────────────────────

@api_market_bp.route('/screener/profit-lock')
@login_required
def get_profit_lock():
    """Profit Lock System — systematic profit-locking with trailing protection."""
    from app.services.profit_lock_system import ProfitLockSystemService
    svc = ProfitLockSystemService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'lock_score_desc'),
        action=request.args.get('action', 'all'),
    ))


# ==========================================================================
#  WAVE 19 — Precision Profit Engine (Features 112-121)
# ==========================================================================

@api_market_bp.route('/screener/probability-matrix')
@login_required
def get_probability_matrix():
    """Feature 112: Probability Matrix — win probability for each asset."""
    from app.services.probability_matrix import ProbabilityMatrixService
    svc = ProbabilityMatrixService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'probability_desc'),
        tier=request.args.get('tier', 'all'),
    ))


@api_market_bp.route('/screener/precision-timer')
@login_required
def get_precision_timer():
    """Feature 113: Precision Timer — optimal entry timing for each asset."""
    from app.services.precision_timer import PrecisionTimerService
    svc = PrecisionTimerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'timing_score_desc'),
        verdict=request.args.get('verdict', 'all'),
    ))


@api_market_bp.route('/screener/profit-velocity')
@login_required
def get_profit_velocity():
    """Feature 114: Profit Velocity Tracker — speed of profit generation."""
    from app.services.profit_velocity import ProfitVelocityService
    svc = ProfitVelocityService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'velocity_score_desc'),
        speed_tier=request.args.get('speed_tier', 'all'),
    ))


@api_market_bp.route('/screener/risk-thermometer')
@login_required
def get_risk_thermometer():
    """Feature 115: Risk Thermometer — danger temperature gauge per asset."""
    from app.services.risk_thermometer import RiskThermometerService
    svc = RiskThermometerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'temperature_asc'),
        temp_zone=request.args.get('temp_zone', 'all'),
    ))


@api_market_bp.route('/screener/signal-convergence')
@login_required
def get_signal_convergence():
    """Feature 116: Signal Convergence Map — count independent bullish signals."""
    from app.services.signal_convergence import SignalConvergenceService
    svc = SignalConvergenceService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'convergence_desc'),
        min_signals=request.args.get('min_signals', 0, type=int),
    ))


@api_market_bp.route('/screener/wealth-compounder')
@login_required
def get_wealth_compounder():
    """Feature 117: Wealth Compounder — compound wealth growth projections."""
    from app.services.wealth_compounder import WealthCompounderService
    svc = WealthCompounderService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'compound_score_desc'),
        projection=request.args.get('projection', 'all'),
    ))


@api_market_bp.route('/screener/gem-finder')
@login_required
def get_gem_finder():
    """Feature 118: Gem Finder — deeply undervalued assets with high potential."""
    from app.services.gem_finder import GemFinderService
    svc = GemFinderService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'gem_score_desc'),
        gem_tier=request.args.get('gem_tier', 'all'),
    ))


@api_market_bp.route('/screener/cycle-sync')
@login_required
def get_cycle_sync():
    """Feature 119: Cycle Synchronizer — synchronized asset cycles for timing."""
    from app.services.cycle_sync import CycleSyncService
    svc = CycleSyncService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'sync_score_desc'),
        phase_filter=request.args.get('phase_filter', 'all'),
    ))


@api_market_bp.route('/screener/smart-sizing')
@login_required
def get_smart_sizing():
    """Feature 120: Smart Sizing Engine — data-driven position sizing."""
    from app.services.smart_sizing import SmartSizingService
    svc = SmartSizingService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'sizing_score_desc'),
        risk_profile=request.args.get('risk_profile', 'moderate'),
    ))


@api_market_bp.route('/screener/wealth-protector')
@login_required
def get_wealth_protector():
    """Feature 121: Wealth Protector — exit signals to protect accumulated wealth."""
    from app.services.wealth_protector import WealthProtectorService
    svc = WealthProtectorService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'danger_desc'),
        action_filter=request.args.get('action_filter', 'all'),
    ))


# ==========================================================================
# Wave 20 — Wealth Certainty Engine (Features 122-131)
# ==========================================================================

@api_market_bp.route('/screener/trade-journal')
@login_required
def get_screener_trade_journal():
    """Feature 122: Trade Journal — signal performance tracking & win rate."""
    from app.services.trade_journal import TradeJournalService
    svc = TradeJournalService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'performance_desc'),
        outcome_filter=request.args.get('outcome', 'all'),
    ))


@api_market_bp.route('/screener/exit-planner')
@login_required
def get_screener_exit_planner():
    """Feature 123: Exit Planner — multi-stage exit strategies per asset."""
    from app.services.exit_planner import ExitPlannerService
    svc = ExitPlannerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'urgency_desc'),
        stage_filter=request.args.get('stage', 'all'),
    ))


@api_market_bp.route('/screener/dca-optimizer')
@login_required
def get_screener_dca_optimizer():
    """Feature 124: DCA Optimizer — dollar cost average recommendations."""
    from app.services.dca_optimizer import DCAOptimizerService
    svc = DCAOptimizerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'dca_score_desc'),
        tier=request.args.get('tier', 'all'),
    ))


@api_market_bp.route('/screener/portfolio-rebalancer')
@login_required
def get_portfolio_rebalancer():
    """Feature 125: Portfolio Rebalancer — drift detection & rebalance suggestions."""
    from app.services.portfolio_rebalancer import PortfolioRebalancerService
    svc = PortfolioRebalancerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'drift_desc'),
        action=request.args.get('action', 'all'),
    ))


@api_market_bp.route('/screener/profit-scheduler')
@login_required
def get_profit_scheduler():
    """Feature 126: Profit Scheduler — systematic profit-taking schedule."""
    from app.services.profit_scheduler import ProfitSchedulerService
    svc = ProfitSchedulerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'schedule_score_desc'),
        urgency=request.args.get('urgency', 'all'),
    ))


@api_market_bp.route('/screener/correlation-radar')
@login_required
def get_correlation_radar():
    """Feature 127: Correlation Radar — asset correlation for true diversification."""
    from app.services.correlation_radar import CorrelationRadarService
    svc = CorrelationRadarService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'diversity_desc'),
        cluster=request.args.get('cluster', 'all'),
    ))


@api_market_bp.route('/screener/regime-detector')
@login_required
def get_regime_detector():
    """Feature 128: Regime Detector — bull/bear/sideways market classification."""
    from app.services.regime_detector import RegimeDetectorService
    svc = RegimeDetectorService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'regime_score_desc'),
        regime=request.args.get('regime', 'all'),
    ))


@api_market_bp.route('/screener/drawdown-shield')
@login_required
def get_drawdown_shield():
    """Feature 129: Drawdown Shield — max drawdown monitor & capital preservation."""
    from app.services.drawdown_shield import DrawdownShieldService
    svc = DrawdownShieldService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'drawdown_desc'),
        severity=request.args.get('severity', 'all'),
    ))


@api_market_bp.route('/screener/risk-parity')
@login_required
def get_screener_risk_parity():
    """Feature 130: Risk Parity Allocator — risk-based allocation optimizer."""
    from app.services.risk_parity import RiskParityService
    svc = RiskParityService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'allocation_desc'),
        quality=request.args.get('quality', 'all'),
    ))


@api_market_bp.route('/screener/stress-tester')
@login_required
def get_stress_tester():
    """Feature 131: Stress Tester — portfolio stress test under crash scenarios."""
    from app.services.stress_tester import StressTesterService
    svc = StressTesterService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'resilience_desc'),
        scenario=request.args.get('scenario', 'moderate'),
    ))


# ═══════════════════════════════════════════════════════════════════
#  WAVE 21 — Tactical Alpha Suite (Features 132-141)
# ═══════════════════════════════════════════════════════════════════

@api_market_bp.route('/screener/momentum-scanner')
@login_required
def get_momentum_scanner():
    """Feature 132: Momentum Scanner — multi-timeframe momentum ranking."""
    from app.services.momentum_scanner import MomentumScannerService
    svc = MomentumScannerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'momentum_desc'),
        tier_filter=request.args.get('tier', 'all'),
    ))


@api_market_bp.route('/screener/value-detector')
@login_required
def get_value_detector():
    """Feature 133: Value Detector — undervalued/overvalued detection."""
    from app.services.value_detector import ValueDetectorService
    svc = ValueDetectorService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'value_desc'),
        valuation_filter=request.args.get('valuation', 'all'),
    ))


@api_market_bp.route('/screener/breakout-predictor')
@login_required
def get_breakout_predictor():
    """Feature 134: Breakout Predictor — breakout probability scoring."""
    from app.services.breakout_predictor import BreakoutPredictorService
    svc = BreakoutPredictorService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'probability_desc'),
        direction=request.args.get('direction', 'all'),
    ))


@api_market_bp.route('/screener/support-resistance-map')
@login_required
def get_support_resistance_map():
    """Feature 135: Support Resistance Map — key S/R level mapping."""
    from app.services.support_resistance_map import SupportResistanceMapService
    svc = SupportResistanceMapService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'proximity_desc'),
        zone=request.args.get('zone', 'all'),
    ))


@api_market_bp.route('/screener/trend-reversal')
@login_required
def get_trend_reversal():
    """Feature 136: Trend Reversal — reversal signal detection."""
    from app.services.trend_reversal import TrendReversalService
    svc = TrendReversalService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'reversal_desc'),
        signal_type=request.args.get('signal_type', 'all'),
    ))


@api_market_bp.route('/screener/volatility-rank')
@login_required
def get_volatility_rank():
    """Feature 137: Volatility Rank — volatility ranking & classification."""
    from app.services.volatility_rank import VolatilityRankService
    svc = VolatilityRankService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'volatility_desc'),
        regime=request.args.get('regime', 'all'),
    ))


@api_market_bp.route('/screener/price-action')
@login_required
def get_price_action():
    """Feature 138: Price Action — price action pattern analysis."""
    from app.services.price_action import PriceActionService
    svc = PriceActionService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'strength_desc'),
        pattern=request.args.get('pattern', 'all'),
    ))


@api_market_bp.route('/screener/market-microstructure')
@login_required
def get_market_microstructure():
    """Feature 139: Market Microstructure — spread & liquidity depth analysis."""
    from app.services.market_microstructure import MarketMicrostructureService
    svc = MarketMicrostructureService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'quality_desc'),
        tier=request.args.get('tier', 'all'),
    ))


@api_market_bp.route('/screener/pair-trader')
@login_required
def get_pair_trader():
    """Feature 140: Pair Trader — pair trading opportunity finder."""
    from app.services.pair_trader import PairTraderService
    svc = PairTraderService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'opportunity_desc'),
        status_filter=request.args.get('status', 'all'),
    ))


@api_market_bp.route('/screener/alpha-composite')
@login_required
def get_alpha_composite():
    """Feature 141: Alpha Composite — composite alpha score from all screeners."""
    from app.services.alpha_composite import AlphaCompositeService
    svc = AlphaCompositeService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'alpha_desc'),
        grade_filter=request.args.get('grade', 'all'),
    ))


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  WAVE 22 — Profit Maximizer Engine (Features 142-151)  🟠 orange theme
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

@api_market_bp.route('/screener/multibagger')
@login_required
def get_multibagger_screener():
    """Feature 142: Multibagger Screener — identify 2x-10x growth potential."""
    from app.services.multibagger_screener import MultibaggerScreener
    svc = MultibaggerScreener()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'score_desc'),
        tier_filter=request.args.get('tier', 'all'),
    ))


@api_market_bp.route('/screener/position-sizer')
@login_required
def get_position_sizer():
    """Feature 143: Position Size Calculator — Kelly Criterion + risk-per-trade."""
    from app.services.position_sizer import PositionSizerService
    svc = PositionSizerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'kelly_desc'),
        sizing_filter=request.args.get('sizing', 'all'),
    ))


@api_market_bp.route('/screener/profit-target')
@login_required
def get_profit_target():
    """Feature 144: Profit Target Optimizer — dynamic TP levels."""
    from app.services.profit_target import ProfitTargetService
    svc = ProfitTargetService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'upside_desc'),
        target_filter=request.args.get('target', 'all'),
    ))


@api_market_bp.route('/screener/dca-planner')
@login_required
def get_dca_planner():
    """Feature 145: DCA Strategy Planner — smart DCA zone allocation."""
    from app.services.dca_planner import DCAStrategyPlanner
    svc = DCAStrategyPlanner()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'dca_score_desc'),
        zone_filter=request.args.get('zone', 'all'),
    ))


@api_market_bp.route('/screener/risk-reward')
@login_required
def get_risk_reward_matrix():
    """Feature 146: Risk/Reward Matrix — R:R analysis for all signals."""
    from app.services.risk_reward_matrix import RiskRewardMatrixService
    svc = RiskRewardMatrixService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'rr_desc'),
        grade_filter=request.args.get('grade', 'all'),
    ))


@api_market_bp.route('/screener/compound-growth')
@login_required
def get_compound_growth_simulator():
    """Feature 147: Compound Growth Simulator — projected portfolio growth."""
    from app.services.compound_growth_simulator import CompoundGrowthSimulator
    svc = CompoundGrowthSimulator()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'cagr_desc'),
        growth_filter=request.args.get('growth', 'all'),
    ))


@api_market_bp.route('/screener/confluence-radar')
@login_required
def get_confluence_radar():
    """Feature 148: Trade Confluence Radar — multi-signal alignment."""
    from app.services.confluence_radar import ConfluenceRadarService
    svc = ConfluenceRadarService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'confluence_desc'),
        conviction_filter=request.args.get('conviction', 'all'),
    ))


@api_market_bp.route('/screener/sector-rotation')
@login_required
def get_sector_rotation_screener():
    """Feature 149: Sector Rotation Tracker — category momentum tracking."""
    from app.services.sector_rotation_tracker import SectorRotationTracker
    svc = SectorRotationTracker()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'rotation_desc'),
        phase_filter=request.args.get('phase', 'all'),
    ))


@api_market_bp.route('/screener/whale-accumulation')
@login_required
def get_whale_accumulation():
    """Feature 150: Whale Accumulation Detector — unusual volume patterns."""
    from app.services.whale_accumulation import WhaleAccumulationDetector
    svc = WhaleAccumulationDetector()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'whale_score_desc'),
        signal_filter=request.args.get('signal', 'all'),
    ))


@api_market_bp.route('/screener/exit-optimizer')
@login_required
def get_exit_optimizer():
    """Feature 151: Exit Strategy Optimizer — trailing stop & partial TP."""
    from app.services.exit_optimizer import ExitOptimizerService
    svc = ExitOptimizerService()
    return jsonify(svc.scan_all(
        asset_type=request.args.get('asset_type', 'crypto'),
        limit=min(request.args.get('limit', 50, type=int), 100),
        page=request.args.get('page', 1, type=int),
        sort_by=request.args.get('sort', 'exit_score_desc'),
        strategy_filter=request.args.get('strategy', 'all'),
    ))
