#!/usr/bin/env python3
"""
A股全市场K线指标扫描器
基于《广通新生300天图解教程》中的指标形态
直接调用东方财富API获取数据
"""
import pandas as pd
import numpy as np
import json
import os
import time
import requests
from datetime import datetime, timedelta
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
import warnings
warnings.filterwarnings('ignore')

OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
RESULT_FILE = os.path.join(OUTPUT_DIR, "scan_results.json")

# 创建全局session
session = requests.Session()
session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
    'Referer': 'https://finance.eastmoney.com/'
})

def get_all_stocks():
    """从东方财富API获取A股全部股票列表（含市值数据）"""
    # 优先从API获取含市值数据
    api_hosts = [
        'http://80.push2.eastmoney.com/api/qt/clist/get',
        'http://push2.eastmoney.com/api/qt/clist/get',
    ]
    all_stocks = []
    for host in api_hosts:
        all_stocks = []
        for page in range(1, 100):
            params = {
                'pn': page,
                'pz': 100,
                'po': 1,
                'np': 1,
                'fltt': 2,
                'invt': 2,
                'fs': 'm:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23',
                'fields': 'f12,f14,f20',
            }
            try:
                resp = session.get(host, params=params, timeout=15)
                data = resp.json()
                stocks = data.get('data', {}).get('diff', [])
                if not stocks:
                    break
                for s in stocks:
                    code = str(s.get('f12', '')).zfill(6)
                    name = s.get('f14', '')
                    raw_cap = s.get('f20', 0)
                    # 处理市值字段可能的非数值（如"-"）
                    try:
                        mkt_cap = float(raw_cap) if raw_cap and raw_cap != '-' else 0
                    except (ValueError, TypeError):
                        mkt_cap = 0
                    if code and name:
                        all_stocks.append({
                            'code': code,
                            'name': name,
                            'mkt_cap': mkt_cap,
                        })
            except Exception as e:
                print(f"  {host} 第{page}页失败: {e}")
                if page == 1:
                    break  # 第一页就失败，换host
                continue
        if all_stocks:
            break  # 成功获取数据，退出host循环

    if not all_stocks:
        # 回退到缓存
        print("API不可用，使用缓存股票列表...")
        cache_file = os.path.join(OUTPUT_DIR, 'stock_list.csv')
        if os.path.exists(cache_file):
            cached = pd.read_csv(cache_file)
            cached = cached.rename(columns={'code': '代码', 'name': '名称'})
            cached['代码'] = cached['代码'].astype(str).str.zfill(6)
            cached['市值'] = 0  # 无市值数据
            cached = cached[~cached['名称'].str.contains(r'ST|\*ST|退|^N|^C', na=False, regex=True)]
            cached = cached[cached['代码'].str.startswith(('00', '30', '60'))]
            cached = cached.reset_index(drop=True)
            print(f"从缓存获取 {len(cached)} 只股票（无市值过滤）")
            return cached[['代码', '名称', '市值']]

    df = pd.DataFrame(all_stocks)
    if df.empty:
        print("无法获取股票列表")
        return df

    df['代码'] = df['code'].astype(str).str.zfill(6)
    df['名称'] = df['name']
    df['市值'] = df['mkt_cap']

    # === 排除条件 ===
    # 1. 排除 ST/*ST/退市/N/C 标识个股
    before_st = len(df)
    df = df[~df['名称'].str.contains(r'ST|\*ST|退|^N|^C', na=False, regex=True)]
    after_st = len(df)
    print(f"排除ST/*ST/退市/N/C: {before_st} -> {after_st} (排除 {before_st - after_st} 只)")

    # 2. 排除市值小于100亿的个股 (100亿 = 10,000,000,000 元)
    before_cap = len(df)
    df = df[df['市值'] >= 10_000_000_000]
    after_cap = len(df)
    print(f"排除市值<100亿: {before_cap} -> {after_cap} (排除 {before_cap - after_cap} 只)")

    # 3. 仅保留沪深主板和创业板
    df = df[df['代码'].str.startswith(('00', '30', '60'))]
    df = df.reset_index(drop=True)
    print(f"最终扫描范围: {len(df)} 只股票（市值>=100亿，排除ST）")
    return df[['代码', '名称', '市值']]

def get_stock_data(code, max_retries=3):
    """通过东方财富API获取个股日K线数据"""
    # 确定市场前缀: 0=深圳, 1=上海
    if code.startswith(('00', '30')):
        secid = f"0.{code}"
    else:
        secid = f"1.{code}"
    
    end_date = datetime.now().strftime('%Y%m%d')
    start_date = (datetime.now() - timedelta(days=300)).strftime('%Y%m%d')
    
    url = 'http://push2his.eastmoney.com/api/qt/stock/kline/get'
    params = {
        'secid': secid,
        'fields1': 'f1,f2,f3,f4,f5,f6',
        'fields2': 'f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61',
        'klt': '101',
        'fqt': '1',
        'beg': start_date,
        'end': end_date,
    }
    
    for attempt in range(max_retries):
        try:
            resp = session.get(url, params=params, timeout=10)
            data = resp.json()
            klines = data.get('data', {}).get('klines', [])
            
            if not klines or len(klines) < 60:
                return None
            
            # 解析K线数据
            # 格式: date,open,close,high,low,volume,amount,amplitude,pct_change,change,turnover
            records = []
            for k in klines:
                parts = k.split(',')
                records.append({
                    'date': parts[0],
                    'open': float(parts[1]),
                    'close': float(parts[2]),
                    'high': float(parts[3]),
                    'low': float(parts[4]),
                    'volume': float(parts[5]),
                    'amount': float(parts[6]),
                })
            
            df = pd.DataFrame(records)
            df = df.reset_index(drop=True)
            return df
        except Exception as e:
            if attempt < max_retries - 1:
                time.sleep(0.5)
            else:
                return None

def calc_ma(series, period):
    return series.rolling(window=period, min_periods=period).mean()

def calc_ema(series, period):
    return series.ewm(span=period, adjust=False).mean()

def calc_macd(close, fast=12, slow=26, signal=9):
    ema_fast = calc_ema(close, fast)
    ema_slow = calc_ema(close, slow)
    dif = ema_fast - ema_slow
    dea = calc_ema(dif, signal)
    macd_bar = 2 * (dif - dea)
    return dif, dea, macd_bar

def detect_indicators(df, code, name):
    """检测所有K线指标形态"""
    results = []
    if df is None or len(df) < 60:
        return results
    
    close = df['close'].values
    high = df['high'].values
    low = df['low'].values
    open_ = df['open'].values
    volume = df['volume'].values
    dates = df['date'].values
    n = len(df)
    
    # 计算均线
    close_s = pd.Series(close)
    ma5 = calc_ma(close_s, 5).values
    ma10 = calc_ma(close_s, 10).values
    ma20 = calc_ma(close_s, 20).values
    ma40 = calc_ma(close_s, 40).values
    ma60 = calc_ma(close_s, 60).values
    ma120 = calc_ma(close_s, 120).values
    
    # 均量线
    vol_s = pd.Series(volume)
    vol_ma5 = calc_ma(vol_s, 5).values
    vol_ma10 = calc_ma(vol_s, 10).values
    vol_ma20 = calc_ma(vol_s, 20).values
    
    # MACD
    dif, dea, macd_bar = calc_macd(close_s)
    dif = dif.values
    dea = dea.values
    
    lookback = 5
    
    for i in range(max(n - lookback, 60), n):
        if np.isnan(ma5[i]) or np.isnan(ma10[i]) or np.isnan(ma20[i]):
            continue
        
        # 1. 价托
        if i >= 1 and not np.isnan(ma5[i-1]) and not np.isnan(ma10[i-1]) and not np.isnan(ma20[i-1]):
            if ma5[i] > ma10[i] and ma5[i] > ma20[i] and ma10[i] > ma20[i]:
                recent_golden = False
                for j in range(max(i-20, 1), i):
                    if (ma5[j] > ma10[j] and ma5[j-1] <= ma10[j-1]) or \
                       (ma5[j] > ma20[j] and ma5[j-1] <= ma20[j-1]) or \
                       (ma10[j] > ma20[j] and ma10[j-1] <= ma20[j-1]):
                        recent_golden = True
                        break
                if recent_golden:
                    results.append({"indicator": "价托", "section": "第1节",
                                   "desc": "5/10/20日均线形成金叉三角形托，空头排列转多头排列",
                                   "type": "看涨", "date": str(dates[i])})
        
        # 2. 量托
        if i >= 1 and not np.isnan(vol_ma5[i]) and not np.isnan(vol_ma10[i]) and not np.isnan(vol_ma20[i]):
            if vol_ma5[i] > vol_ma10[i] and vol_ma5[i] > vol_ma20[i] and vol_ma10[i] > vol_ma20[i]:
                recent_vol_cross = False
                for j in range(max(i-20, 1), i):
                    if (vol_ma5[j] > vol_ma10[j] and vol_ma5[j-1] <= vol_ma10[j-1]) or \
                       (vol_ma10[j] > vol_ma20[j] and vol_ma10[j-1] <= vol_ma20[j-1]):
                        recent_vol_cross = True
                        break
                if recent_vol_cross:
                    results.append({"indicator": "量托", "section": "第2节",
                                   "desc": "5/10/20日均量线形成金叉三角形，成交量呈均态上升",
                                   "type": "看涨", "date": str(dates[i])})
        
        # 3. 多方炮
        if i >= 2:
            if (close[i-2] > open_[i-2] and close[i-1] < open_[i-1] and close[i] > open_[i] and
                close[i] > open_[i-1]):
                results.append({"indicator": "多方炮", "section": "第3节",
                               "desc": "两阳夹一阴K线组合，多方发力向上攻击",
                               "type": "看涨", "date": str(dates[i])})
        
        # 4. 价压
        if i >= 1 and not np.isnan(ma5[i-1]) and not np.isnan(ma10[i-1]) and not np.isnan(ma20[i-1]):
            if ma5[i] < ma10[i] and ma5[i] < ma20[i] and ma10[i] < ma20[i]:
                recent_death = False
                for j in range(max(i-20, 1), i):
                    if (ma5[j] < ma10[j] and ma5[j-1] >= ma10[j-1]) or \
                       (ma10[j] < ma20[j] and ma10[j-1] >= ma20[j-1]):
                        recent_death = True
                        break
                if recent_death:
                    results.append({"indicator": "价压", "section": "第12节",
                                   "desc": "5/10/20日均线形成死叉三角形压，多头转空头排列",
                                   "type": "看跌", "date": str(dates[i])})
        
        # 5. 量压
        if i >= 1 and not np.isnan(vol_ma5[i]) and not np.isnan(vol_ma10[i]) and not np.isnan(vol_ma20[i]):
            if vol_ma5[i] < vol_ma10[i] and vol_ma5[i] < vol_ma20[i] and vol_ma10[i] < vol_ma20[i]:
                recent_vol_death = False
                for j in range(max(i-20, 1), i):
                    if (vol_ma5[j] < vol_ma10[j] and vol_ma5[j-1] >= vol_ma10[j-1]) or \
                       (vol_ma10[j] < vol_ma20[j] and vol_ma10[j-1] >= vol_ma20[j-1]):
                        recent_vol_death = True
                        break
                if recent_vol_death:
                    results.append({"indicator": "量压", "section": "第15节",
                                   "desc": "5/10/20日均量线形成死叉三角形，成交量递减",
                                   "type": "看跌", "date": str(dates[i])})
        
        # 6. 金蜘蛛
        if not np.isnan(ma5[i]) and not np.isnan(ma10[i]) and not np.isnan(ma20[i]):
            spread = max(abs(ma5[i]-ma10[i]), abs(ma5[i]-ma20[i]), abs(ma10[i]-ma20[i]))
            if spread < close[i] * 0.015 and ma5[i] > ma5[max(i-1,0)] and ma10[i] > ma10[max(i-1,0)]:
                results.append({"indicator": "金蜘蛛", "section": "第16节",
                               "desc": "5/10/20日均线在某一价位附近交汇后向上发散",
                               "type": "看涨", "date": str(dates[i])})
        
        # 7. 死蜘蛛
        if not np.isnan(ma5[i]) and not np.isnan(ma10[i]) and not np.isnan(ma20[i]):
            spread = max(abs(ma5[i]-ma10[i]), abs(ma5[i]-ma20[i]), abs(ma10[i]-ma20[i]))
            if spread < close[i] * 0.015 and ma5[i] < ma5[max(i-1,0)] and ma10[i] < ma10[max(i-1,0)]:
                results.append({"indicator": "死蜘蛛", "section": "第17节",
                               "desc": "5/10/20日均线交汇后向下发散",
                               "type": "看跌", "date": str(dates[i])})
        
        # 8. 出水芙蓉
        if not np.isnan(ma60[i]) and not np.isnan(vol_ma20[i]):
            if close[i] > ma60[i] and open_[i] < ma60[i] and volume[i] > vol_ma20[i] * 1.5:
                below_before = any(close[j] < ma60[j] for j in range(max(i-10, 0), i) if not np.isnan(ma60[j]))
                if below_before:
                    results.append({"indicator": "出水芙蓉", "section": "第20节",
                                   "desc": "放量阳线突破60日均线，切断长期跌势",
                                   "type": "看涨", "date": str(dates[i])})
        
        # 9. 断头铡刀
        if i >= 1 and not np.isnan(ma5[i]) and not np.isnan(ma10[i]) and not np.isnan(ma20[i]):
            if (close[i] < ma5[i] and close[i] < ma10[i] and close[i] < ma20[i] and
                open_[i] > ma5[i] and close[i] < open_[i]):
                results.append({"indicator": "断头铡刀", "section": "第21节",
                               "desc": "一根阴线同时跌破5/10/20日三条均线",
                               "type": "看跌", "date": str(dates[i])})
        
        # 10. 空方炮
        if i >= 2:
            if (close[i-2] < open_[i-2] and close[i-1] > open_[i-1] and close[i] < open_[i] and
                close[i] < open_[i-1]):
                results.append({"indicator": "空方炮", "section": "第22节",
                               "desc": "两阴夹一阳K线组合，空方发力向下打压",
                               "type": "看跌", "date": str(dates[i])})
        
        # 11. 量顶天立地
        if i >= 60:
            max_vol = max(volume[max(i-120,0):i]) if i >= 120 else max(volume[:i])
            if volume[i] >= max_vol:
                results.append({"indicator": "量顶天立地", "section": "第23节",
                               "desc": "成交量创近120日新高，主力大量建仓或出货",
                               "type": "信号", "date": str(dates[i])})
        
        # 12. 三金叉见底
        if i >= 1 and not np.isnan(dif[i]) and not np.isnan(dea[i]):
            window = 5
            ma_cross = any(ma5[j] > ma10[j] and ma5[j-1] <= ma10[j-1] for j in range(max(i-window,1), i+1) if not np.isnan(ma5[j-1]) and not np.isnan(ma10[j-1]))
            vol_cross = any(vol_ma5[j] > vol_ma10[j] and vol_ma5[j-1] <= vol_ma10[j-1] for j in range(max(i-window,1), i+1) if not np.isnan(vol_ma5[j-1]) and not np.isnan(vol_ma10[j-1]))
            macd_cross = any(dif[j] > dea[j] and dif[j-1] <= dea[j-1] for j in range(max(i-window,1), i+1) if not np.isnan(dif[j-1]) and not np.isnan(dea[j-1]))
            if ma_cross and vol_cross and macd_cross:
                results.append({"indicator": "三金叉见底", "section": "第46节",
                               "desc": "均线金叉+均量金叉+MACD金叉同时出现，股价见底信号",
                               "type": "看涨", "date": str(dates[i])})
        
        # 13. 三死叉见顶
        if i >= 1 and not np.isnan(dif[i]) and not np.isnan(dea[i]):
            window = 5
            ma_death = any(ma5[j] < ma10[j] and ma5[j-1] >= ma10[j-1] for j in range(max(i-window,1), i+1) if not np.isnan(ma5[j-1]) and not np.isnan(ma10[j-1]))
            vol_death = any(vol_ma5[j] < vol_ma10[j] and vol_ma5[j-1] >= vol_ma10[j-1] for j in range(max(i-window,1), i+1) if not np.isnan(vol_ma5[j-1]) and not np.isnan(vol_ma10[j-1]))
            macd_death = any(dif[j] < dea[j] and dif[j-1] >= dea[j-1] for j in range(max(i-window,1), i+1) if not np.isnan(dif[j-1]) and not np.isnan(dea[j-1]))
            if ma_death and vol_death and macd_death:
                results.append({"indicator": "三死叉见顶", "section": "第47节",
                               "desc": "均线死叉+均量死叉+MACD死叉同时出现，股价见顶信号",
                               "type": "看跌", "date": str(dates[i])})
        
        # 14. DIF上穿零线
        if i >= 1 and not np.isnan(dif[i]) and not np.isnan(dif[i-1]):
            if dif[i] > 0 and dif[i-1] <= 0:
                results.append({"indicator": "DIF上穿零线", "section": "第52节",
                               "desc": "MACD的DIF由负转正，空头市场可能转多头",
                               "type": "看涨", "date": str(dates[i])})
        
        # 15. DIF下穿零线
        if i >= 1 and not np.isnan(dif[i]) and not np.isnan(dif[i-1]):
            if dif[i] < 0 and dif[i-1] >= 0:
                results.append({"indicator": "DIF下穿零线", "section": "第51节",
                               "desc": "MACD的DIF由正转负，多头市场可能转空头",
                               "type": "看跌", "date": str(dates[i])})
        
        # 16. 水上金叉
        if i >= 1 and not np.isnan(dif[i]) and not np.isnan(dea[i]):
            if dif[i] > dea[i] and dif[i-1] <= dea[i-1] and dif[i] > 0:
                results.append({"indicator": "水上金叉", "section": "第217节",
                               "desc": "MACD在零轴上方金叉，强势上涨信号",
                               "type": "看涨", "date": str(dates[i])})
        
        # 17. 水下死叉
        if i >= 1 and not np.isnan(dif[i]) and not np.isnan(dea[i]):
            if dif[i] < dea[i] and dif[i-1] >= dea[i-1] and dif[i] < 0:
                results.append({"indicator": "水下死叉", "section": "第189节",
                               "desc": "MACD在零轴下方死叉，加速下跌信号",
                               "type": "看跌", "date": str(dates[i])})
        
        # 18. 五线顺下
        if i >= 1 and not np.isnan(ma120[i]):
            all_down = (ma5[i] < ma5[i-1] and ma10[i] < ma10[i-1] and 
                       ma20[i] < ma20[i-1] and ma60[i] < ma60[i-1] and ma120[i] < ma120[i-1])
            aligned = (ma120[i] > ma60[i] > ma20[i] > ma10[i] > ma5[i])
            if all_down and aligned:
                results.append({"indicator": "五线顺下", "section": "第150节",
                               "desc": "5/10/20/60/120日均线全部向下排列，暴跌区间",
                               "type": "看跌", "date": str(dates[i])})
        
        # 19. 进入2+3区间
        if not np.isnan(ma60[i]) and not np.isnan(ma120[i]):
            if close[i] > ma60[i] and close[i] > ma120[i] and ma60[i] > ma120[i]:
                if i >= 1 and not np.isnan(ma60[i-1]) and not np.isnan(ma120[i-1]):
                    if close[i-1] <= ma60[i-1] or close[i-1] <= ma120[i-1]:
                        results.append({"indicator": "进入2+3区间", "section": "第115节",
                                       "desc": "股价站上60日和120日均线之上，进入强势区间",
                                       "type": "看涨", "date": str(dates[i])})
        
        # 20. 老鸭头
        if not np.isnan(ma60[i]) and i >= 5:
            neck = ma5[i] > ma60[i] and ma10[i] > ma60[i]
            duck_nose = False
            for j in range(max(i-10, 1), i):
                if j+1 < n and ma5[j] < ma10[j] and ma5[j+1] > ma10[j+1]:
                    duck_nose = True
                    break
            if neck and duck_nose and ma5[i] > ma10[i]:
                results.append({"indicator": "老鸭头", "section": "第26节",
                               "desc": "5/10日均线放量上穿60日均线后回落再金叉，经典洗盘形态",
                               "type": "看涨", "date": str(dates[i])})
        
        # 21. 倒挂老鸭头
        if not np.isnan(ma60[i]) and i >= 1:
            if ma5[i] < ma60[i] and ma10[i] < ma60[i]:
                if ma5[i-1] >= ma60[i-1] or ma10[i-1] >= ma60[i-1]:
                    results.append({"indicator": "倒挂老鸭头", "section": "第27节",
                                   "desc": "5/10日均线跌破60日均线，头部形态",
                                   "type": "看跌", "date": str(dates[i])})
        
        # 22. 底部芝麻功
        if not np.isnan(vol_ma5[i]) and not np.isnan(vol_ma10[i]) and not np.isnan(vol_ma20[i]):
            if volume[i] < vol_ma5[i] * 0.5 and volume[i] < vol_ma10[i] * 0.5 and volume[i] < vol_ma20[i] * 0.5:
                results.append({"indicator": "底部芝麻功", "section": "第5节",
                               "desc": "成交量极度萎缩至均量线50%以下，接近底部区间",
                               "type": "看涨", "date": str(dates[i])})
        
        # 23. 东方红大阳升
        if not np.isnan(vol_ma20[i]) and not np.isnan(ma20[i]):
            if volume[i] > vol_ma20[i] * 2.0 and (close[i] - open_[i]) / open_[i] > 0.05 and close[i] > ma20[i]:
                results.append({"indicator": "东方红大阳升", "section": "第4节",
                               "desc": "巨量大阳线突破均线系统，庄家密集建仓",
                               "type": "看涨", "date": str(dates[i])})
        
        # 24. 放量过头
        if i >= 20 and not np.isnan(vol_ma20[i]):
            prev_high = max(high[max(i-20,0):i])
            if close[i] > prev_high and volume[i] > vol_ma20[i] * 1.5:
                results.append({"indicator": "放量过头", "section": "第42节",
                               "desc": "放量突破前期高点，主力增加建仓力度",
                               "type": "看涨", "date": str(dates[i])})
        
        # 25. 轻松过头
        if i >= 20:
            prev_high = max(high[max(i-20,0):i])
            prev_max_vol = max(volume[max(i-20,0):i])
            if close[i] > prev_high and volume[i] < prev_max_vol * 0.5:
                results.append({"indicator": "轻松过头", "section": "第43节",
                               "desc": "缩量突破前期高点，筹码已锁定",
                               "type": "看涨", "date": str(dates[i])})
        
        # 26. 一阳上穿三角托
        if i >= 1 and not np.isnan(ma5[i]) and not np.isnan(ma10[i]) and not np.isnan(ma20[i]):
            if (close[i] > ma5[i] and close[i] > ma10[i] and close[i] > ma20[i] and
                open_[i] < ma5[i] and open_[i] < ma10[i] and open_[i] < ma20[i] and close[i] > open_[i]):
                results.append({"indicator": "一阳上穿三角托", "section": "第19节",
                               "desc": "一根阳线向上穿过5/10/20日三条均线",
                               "type": "看涨", "date": str(dates[i])})
        
        # 27. 一阴下穿三角压
        if i >= 1 and not np.isnan(ma5[i]) and not np.isnan(ma10[i]) and not np.isnan(ma20[i]):
            if (close[i] < ma5[i] and close[i] < ma10[i] and close[i] < ma20[i] and
                open_[i] > ma5[i] and open_[i] > ma10[i] and open_[i] > ma20[i] and close[i] < open_[i]):
                results.append({"indicator": "一阴下穿三角压", "section": "第18节",
                               "desc": "一根阴线向下穿过5/10/20日三条均线",
                               "type": "看跌", "date": str(dates[i])})
        
        # 28. 天眼地量
        if i >= 1 and not np.isnan(ma20[i]) and not np.isnan(vol_ma20[i]):
            spread = abs(ma5[i] - ma20[i]) / ma20[i] if ma20[i] != 0 else 1
            if spread < 0.01 and volume[i] < vol_ma20[i] * 0.5:
                results.append({"indicator": "天眼地量", "section": "第63节",
                               "desc": "5日与20日均线极近形成天眼，成交量地量",
                               "type": "看涨", "date": str(dates[i])})
        
        # 29. 熊牛转换点
        if i >= 1 and not np.isnan(ma60[i]) and not np.isnan(ma120[i]) and not np.isnan(ma60[i-1]) and not np.isnan(ma120[i-1]):
            if ma60[i] > ma120[i] and ma60[i-1] <= ma120[i-1]:
                results.append({"indicator": "熊牛转换点", "section": "第116节",
                               "desc": "60日均线上穿120日均线，熊市转牛市",
                               "type": "看涨", "date": str(dates[i])})
        
        # 30. 牛熊转换点
        if i >= 1 and not np.isnan(ma60[i]) and not np.isnan(ma120[i]) and not np.isnan(ma60[i-1]) and not np.isnan(ma120[i-1]):
            if ma60[i] < ma120[i] and ma60[i-1] >= ma120[i-1]:
                results.append({"indicator": "牛熊转换点", "section": "第120节",
                               "desc": "60日均线下穿120日均线，牛市转熊市",
                               "type": "看跌", "date": str(dates[i])})
        
        # 31. 阳阴墓碑
        if i >= 1 and i >= 20:
            if (close[i-1] > open_[i-1] and close[i] < open_[i] and close[i] < open_[i-1]):
                recent_high = max(high[max(i-20,0):i])
                if close[i] > recent_high * 0.93:
                    results.append({"indicator": "阳阴墓碑", "section": "第98节",
                                   "desc": "高位阳线后紧接阴线，头部信号",
                                   "type": "看跌", "date": str(dates[i])})
        
        # 32. 三连阴
        if i >= 2:
            if close[i] < open_[i] and close[i-1] < open_[i-1] and close[i-2] < open_[i-2]:
                avg_body = ((open_[i]-close[i]) + (open_[i-1]-close[i-1]) + (open_[i-2]-close[i-2])) / 3
                if avg_body / close[i] > 0.01:
                    results.append({"indicator": "三连阴", "section": "第106节",
                                   "desc": "连续三根较长阴线，空方开始主导",
                                   "type": "看跌", "date": str(dates[i])})
        
        # 33. 多连阴
        if i >= 10:
            yin_count = sum(1 for j in range(max(i-10,0), i+1) if close[j] < open_[j])
            if yin_count >= 7:
                results.append({"indicator": "多连阴", "section": "第110节",
                               "desc": "近10根K线中阴线占多数，空方主导",
                               "type": "看跌", "date": str(dates[i])})
        
        # 34. 会创新高还有新高
        if i >= 5:
            highs = [high[j] for j in range(max(i-5,0), i+1)]
            if all(highs[j] >= highs[j-1] for j in range(1, len(highs))):
                results.append({"indicator": "会创新高还有新高", "section": "第144节",
                               "desc": "股价持续创出新高，上涨趋势延续",
                               "type": "看涨", "date": str(dates[i])})
        
        # 35. 会创新低还有新低
        if i >= 5:
            lows = [low[j] for j in range(max(i-5,0), i+1)]
            if all(lows[j] <= lows[j-1] for j in range(1, len(lows))):
                results.append({"indicator": "会创新低还有新低", "section": "第172节",
                               "desc": "股价持续创出新低，下跌趋势延续",
                               "type": "看跌", "date": str(dates[i])})
        
        # 36. 压转托
        if i >= 30 and not np.isnan(ma5[i]) and not np.isnan(ma10[i]) and not np.isnan(ma20[i]):
            if ma5[i] > ma10[i] and ma10[i] > ma20[i]:
                if not np.isnan(ma5[i-30]) and ma5[i-30] < ma10[i-30] < ma20[i-30]:
                    results.append({"indicator": "压转托", "section": "第53节",
                                   "desc": "价压后均线重新多头排列，强庄洗盘后拉升",
                                   "type": "看涨", "date": str(dates[i])})
        
        # 37. 托转压
        if i >= 30 and not np.isnan(ma5[i]) and not np.isnan(ma10[i]) and not np.isnan(ma20[i]):
            if ma5[i] < ma10[i] and ma10[i] < ma20[i]:
                if not np.isnan(ma5[i-30]) and ma5[i-30] > ma10[i-30] > ma20[i-30]:
                    results.append({"indicator": "托转压", "section": "第54节",
                                   "desc": "价托后均线重新空头排列，庄家出货",
                                   "type": "看跌", "date": str(dates[i])})
        
        # 38. 均线多头排列
        if not np.isnan(ma5[i]) and not np.isnan(ma10[i]) and not np.isnan(ma20[i]) and not np.isnan(ma60[i]):
            if ma5[i] > ma10[i] > ma20[i] > ma60[i] and close[i] > ma5[i]:
                if i >= 1 and not np.isnan(ma5[i-1]):
                    if not (ma5[i-1] > ma10[i-1] > ma20[i-1] > ma60[i-1]):
                        results.append({"indicator": "均线多头排列", "section": "第6节",
                                       "desc": "5/10/20/60日均线多头排列，上涨趋势确立",
                                       "type": "看涨", "date": str(dates[i])})
        
        # 39. 均线空头排列
        if not np.isnan(ma5[i]) and not np.isnan(ma10[i]) and not np.isnan(ma20[i]) and not np.isnan(ma60[i]):
            if ma5[i] < ma10[i] < ma20[i] < ma60[i] and close[i] < ma5[i]:
                if i >= 1 and not np.isnan(ma5[i-1]):
                    if not (ma5[i-1] < ma10[i-1] < ma20[i-1] < ma60[i-1]):
                        results.append({"indicator": "均线空头排列", "section": "第6节",
                                       "desc": "5/10/20/60日均线空头排列，下跌趋势确立",
                                       "type": "看跌", "date": str(dates[i])})
        
        # 40. 巨量长阳
        if not np.isnan(vol_ma20[i]) and close[i] > open_[i]:
            body = (close[i] - open_[i]) / open_[i]
            if body > 0.05 and volume[i] > vol_ma20[i] * 2.0:
                results.append({"indicator": "巨量长阳", "section": "第213节",
                               "desc": "大阳线配合巨量成交，资金积极入场",
                               "type": "看涨", "date": str(dates[i])})
    
    # 去重
    seen = set()
    unique_results = []
    for r in reversed(results):
        key = r["indicator"]
        if key not in seen:
            seen.add(key)
            unique_results.append(r)
    return list(reversed(unique_results))

def process_stock(code, name, mkt_cap=0):
    try:
        df = get_stock_data(code)
        if df is not None:
            signals = detect_indicators(df, code, name)
            if signals:
                return {
                    "code": code,
                    "name": name,
                    "mkt_cap": mkt_cap,
                    "mkt_cap_yi": round(mkt_cap / 1_0000_0000, 1) if mkt_cap else 0,  # 市值(亿元)
                    "signals": signals,
                    "signal_count": len(signals),
                    "latest_price": float(df['close'].iloc[-1]),
                    "pct_change": float(df['close'].iloc[-1] / df['close'].iloc[-2] - 1) * 100 if len(df) > 1 else 0,
                }
    except:
        pass
    return None

def main():
    stock_list = get_all_stocks()
    if stock_list.empty:
        print("无法获取股票列表")
        return
    
    total = len(stock_list)
    print(f"开始扫描 {total} 只股票（并发10线程）...")
    
    all_results = []
    completed = 0
    lock = threading.Lock()
    
    tasks = [(row['代码'], row['名称'], row['市值']) for _, row in stock_list.iterrows()]
    
    with ThreadPoolExecutor(max_workers=10) as executor:
        futures = {executor.submit(process_stock, code, name, cap): (code, name) 
                   for code, name, cap in tasks}
        
        for future in as_completed(futures):
            completed += 1
            result = future.result()
            if result:
                with lock:
                    all_results.append(result)
            
            if completed % 200 == 0:
                print(f"进度: {completed}/{total} ({completed/total*100:.1f}%) - 有信号股票: {len(all_results)}")
                with open(RESULT_FILE, 'w', encoding='utf-8') as f:
                    temp = sorted(all_results, key=lambda x: x["signal_count"], reverse=True)
                    json.dump({
                        "scan_date": datetime.now().strftime('%Y-%m-%d %H:%M'),
                        "total_scanned": completed,
                        "total_with_signals": len(all_results),
                        "top50": temp[:50],
                        "all_results": temp
                    }, f, ensure_ascii=False, indent=2)
    
    all_results.sort(key=lambda x: x["signal_count"], reverse=True)
    
    with open(RESULT_FILE, 'w', encoding='utf-8') as f:
        json.dump({
            "scan_date": datetime.now().strftime('%Y-%m-%d %H:%M'),
            "total_scanned": total,
            "total_with_signals": len(all_results),
            "top50": all_results[:50],
            "all_results": all_results
        }, f, ensure_ascii=False, indent=2)
    
    print(f"\n扫描完成！共扫描 {total} 只股票，{len(all_results)} 只有信号")
    print(f"前50只已保存到 {RESULT_FILE}")

if __name__ == "__main__":
    main()
