Skill: dnaxxx-hub/a-stock-quant-toolkit (version 1.0.0)

# 📈 A股量化工具包 (Finance Toolkit) A股量化交易工具包，基于腾讯行情API（零依赖、免注册），提供完整的实时行情、技术指标、策略回测、监控评分、日报生成功能。 ## 核心模块 | 模块 | 文件 | 功能 | |------|------|------| | 🚀 实时行情 | `mini_realtime.py` | 腾讯API直取实时报价/日K/分时，零依赖 | | 📊 数据获取 | `astock_data.py` | A股全市场数据获取（基于akshare，需安装akshare） | | ⚙️ 数据引擎 | `astock_engine.py` | 腾讯行情API封装，含缓存和curl调用 | | 🧮 策略集 | `astock_strategies.py` | 均线金叉/RSI/布林带/KDJ等策略 | | 🔬 回测系统 | `backtest_v3.py` | 10个策略一键回测+参数搜索+敏感性分析 | | 📡 实时监控 | `monitor_v3.py` | 6维60分评分体系+FFT频谱分析 | | 🎯 策略引擎 | `strategy_v4.py` | 多指标融合策略（布林带+RSI+KDJ+MACD） | | 📋 日报系统 | `daily_report.py` | 自动生成A股市场分析日报 | | 🔄 FFT分析 | `fourier_analyzer.py` | K线FFT频谱分析，识别主周期 | | ⚡ 盯盘系统 | `strategy_engine.py` | 腾讯API实时盯盘，技术位判断 | ## 依赖 **最小运行时（实时行情/监控/FFT分析）：零外部依赖**，仅需 Python 3 标准库。 - `mini_realtime.py` — 仅用 `urllib`、`json`、`struct` - `monitor_v3.py` — 仅用标准库 - `fourier_analyzer.py` — 纯Python FFT **完整功能需要：** - pandas、numpy（回测和多指标分析必需） - akshare（仅 `astock_data.py` 需要，可用 `mini_realtime.py` 替代） - matplotlib、mplfinance（仅K线图报告 `kline_report.py` 需要） ## 快速开始 ### 1. 实时行情（零依赖） ```python from finance_toolkit.mini_realtime import TencentStockAPI api = TencentStockAPI() # 获取实时报价 q = api.get_quote("000009") print(f"{q['name']}: ¥{q['price']} ({q['change_pct']:+.2f}%)") # 获取60日日K线 klines = api.get_klines("000009", 60) closes = [k['close'] for k in klines] print(f"最新收盘价: {closes[-1]}") ``` ### 2. 60分评分体系 ```python from finance_toolkit.mini_realtime import TencentStockAPI, calc_sma, calc_rsi from finance_toolkit.monitor_v3 import score_stock api = TencentStockAPI() result = score_stock("000009", api) print(f"评分: {result['score']}/60") print(f"信号: {result['signals']}") ``` ### 3. 一键回测10个策略 ```python from finance_toolkit.backtest_v3 import DataFetcher, BacktestEngine, BacktestReport fetcher = DataFetcher() df = fetcher.get("sz000009", 500) # 获取500日K线 dp = BacktestEngine._prepare(df) # 10个策略对比排名 results = BacktestReport.brief(dp, "中国宝安") ``` ### 4. 实时监控+FFT分析 ```python from finance_toolkit.monitor_v3 import monitor_all from finance_toolkit.mini_realtime import TencentStockAPI from finance_toolkit.fourier_analyzer import analyze_spectrum, get_strategy_hints # 监控所有股票 results = monitor_all() # FFT频谱分析 api = TencentStockAPI() klines = api.get_klines("000009", 200) closes = [k['close'] for k in klines] spectrum = analyze_spectrum(closes) hints = get_strategy_hints(spectrum) print(f"主周期: {hints['dominant_cycle']}天") print(f"建议: {hints['advice']}") ``` ### 5. 策略引擎（多指标融合） ```python from finance_toolkit.strategy_v4 import DataSource, Backtest ds = DataSource() df = ds.get_kline("sz000009", 500) bt = Backtest(df) result = bt.run("bollinger_rsi_fusion") print(result['metrics']) ``` ### 6. 日报生成 ```python from finance_toolkit.daily_report import generate_daily_report report = generate_daily_report() print(report['market_overview']) ``` ## 命令行用法 ```bash # 快速回测 python -m finance_toolkit.backtest_v3 brief # 完整回测+分段分析 python -m finance_toolkit.backtest_v3 full # 参数扫描优化 python -m finance_toolkit.backtest_v3 scan # 融合策略参数搜索 python -m finance_toolkit.backtest_v3 fusion_scan ``` ## 数据源说明 所有模块默认使用 **腾讯行情API**（`qt.gtimg.cn`），优势： - ✅ 无需注册，无需API密钥 - ✅ 免费，无调用限制 - ✅ 实时数据，延迟<1秒 - ✅ 支持沪深全市场股票 ## 注意事项 - 股票代码格式：沪市 `sh600519`，深市 `sz000009`（腾讯API）或直接 `000009`（mini_realtime） - 回测使用前复权数据 - 交易策略仅供参考，实盘需谨慎 - 工作日9:30-15:00为实时交易时段，非交易时段返回最新收盘价

path: finance_toolkit/astock_data.py
content: """
A股数据获取模块 - 基于akshare
支持沪深全市场数据，含缓存机制
"""

import akshare as ak
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import os
import json
import logging

logger = logging.getLogger(__name__)

class AStockDataFetcher:
    """A股数据获取器"""

    def __init__(self, cache_dir="data/cache"):
        self.cache_dir = cache_dir
        os.makedirs(cache_dir, exist_ok=True)

    def get_real_time_quote(self, symbol="sh600519"):
        """获取实时行情"""
        try:
            df = ak.stock_zh_a_spot_em()
            if symbol:
                df = df[df['代码'] == symbol.replace('sh','').replace('sz','')]
            return df
        except Exception as e:
            logger.error(f"获取实时行情失败: {e}")
            return None

    def get_history(self, symbol="600519", start_date=None, end_date=None, period="daily"):
        """获取历史K线数据"""
        try:
            if start_date is None:
                start_date = (datetime.now() - timedelta(days=365)).strftime("%Y%m%d")
            if end_date is None:
                end_date = datetime.now().strftime("%Y%m%d")

            df = ak.stock_zh_a_hist(
                symbol=symbol,
                period=period,
                start_date=start_date,
                end_date=end_date,
                adjust="qfq"  # 前复权
            )
            if df is not None and not df.empty:
                df['代码'] = symbol
            return df
        except Exception as e:
            logger.error(f"获取{symbol}历史数据失败: {e}")
            return None

    def get_index_data(self, symbol="sh000001", start_date=None, end_date=None):
        """获取指数数据"""
        try:
            if start_date is None:
                start_date = (datetime.now() - timedelta(days=365)).strftime("%Y%m%d")
            if end_date is None:
                end_date = datetime.now().strftime("%Y%m%d")

            df = ak.stock_zh_index_daily(symbol=symbol)
            df = df[(df['date'] >= start_date) & (df['date'] <= end_date)]
            return df
        except Exception as e:
            logger.error(f"获取指数数据失败: {e}")
            return None

    def get_stock_list(self):
        """获取A股全部股票列表"""
        try:
            df = ak.stock_zh_a_spot_em()
            return df[['代码','名称','最新价','涨跌幅','成交量','成交额','换手率','市盈率-动态','总市值']]
        except Exception as e:
            logger.error(f"获取股票列表失败: {e}")
            return None

    def get_hot_stocks(self, top_n=50):
        """获取热门股票（按成交额排序）"""
        df = self.get_stock_list()
        if df is not None:
            df = df.sort_values('成交额', ascending=False).head(top_n)
        return df

    def get_concept_board(self, board_name=None):
        """获取概念板块"""
        try:
            df = ak.stock_board_concept_name_em()
            if board_name:
                df = df[df['板块名称'].str.contains(board_name)]
            return df
        except Exception as e:
            logger.error(f"获取概念板块失败: {e}")
            return None

if __name__ == "__main__":
    fetcher = AStockDataFetcher()
    print("=== A股数据获取模块测试 ===")

    # 测试实时行情
    hot = fetcher.get_hot_stocks(5)
    if hot is not None:
        print(f"\n📊 热门股票 Top5:")
        print(hot[['代码','名称','最新价','涨跌幅','成交额','换手率']].to_string(index=False))

    # 测试历史数据
    df = fetcher.get_history("600519", start_date="20260101")
    if df is not None:
        print(f"\n📈 贵州茅台历史数据 (最近3行):")
        print(df.tail(3)[['日期','开盘','收盘','最高','最低','成交量','换手率']].to_string(index=False))

    # 测试概念板块
    board = fetcher.get_concept_board()
    if board is not None:
        print(f"\n🏷️ 概念板块总数: {len(board)}")
        print(f"    涨幅前5: {board.sort_values('涨跌幅',ascending=False).head(5)['板块名称'].tolist()}")


path: finance_toolkit/astock_engine.py
content: #!/usr/bin/env python3
"""
A股量化数据引擎 - 基于腾讯行情API
稳定、免注册、速度快
"""

import subprocess
import json
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import os
import re
import time
import logging

logger = logging.getLogger(__name__)

class AStockEngine:
    """A股数据引擎"""

    def __init__(self, cache_dir="data/cache"):
        self.cache_dir = cache_dir
        os.makedirs(cache_dir, exist_ok=True)

    def _curl(self, url, timeout=10):
        """通过curl获取数据"""
        try:
            result = subprocess.run(
                ['curl.exe', '-s', '--connect-timeout', str(timeout), url],
                capture_output=True, timeout=timeout+5
            )
            if result.returncode == 0 and result.stdout:
                return result.stdout.decode('gbk', errors='ignore')
            return None
        except:
            return None

    def get_quotes(self, symbols):
        """批量获取行情"""
        if not symbols:
            return []
        codes = ','.join(symbols)
        url = f"http://qt.gtimg.cn/q={codes}"
        data = self._curl(url)
        if not data:
            return []
        results = []
        for line in data.strip().strip(';').split(';'):
            if '="' in line:
                r = self._parse(line + ';')
                if r:
                    results.append(r)
        return results

    def _parse(self, raw):
        """解析腾讯行情数据"""
        try:
            m = re.search(r'v_[^=]+="([^"]+)"', raw)
            if not m:
                return None
            p = m.group(1).split('~')
            if len(p) < 40:
                return None
            return {
                'name': p[1], 'code': p[2],
                'price': float(p[3]) if p[3] else 0,
                'change_pct': round(float(p[32]) if p[32] else 0, 2),
                'volume': int(p[6]) if p[6] else 0,
                'amount': float(p[37]) if len(p) > 37 and p[37] else 0,
                'high': float(p[33]) if p[33] else 0,
                'low': float(p[34]) if p[34] else 0,
                'open': float(p[5]) if p[5] else 0,
                'yclose': float(p[4]) if p[4] else 0,
                'turnover': float(p[38]) if len(p) > 38 and p[38] else 0,
                'pe': float(p[39]) if len(p) > 39 and p[39] else 0,
                'market_cap': float(p[45]) if len(p) > 45 and p[45] else 0,
            }
        except:
            return None

    def get_index(self):
        """获取主要指数"""
        syms = ['sh000001','sz399001','sz399006','sh000300','sh000016','sh000688']
        names = {'000001':'上证指数','399001':'深证成指','399006':'创业板指',
                 '000300':'沪深300','000016':'上证50','000688':'科创50'}
        data = self.get_quotes(syms)
        return {names.get(d['code'], d['code']): d for d in data}

    def get_hot_stocks(self):
        """获取热门自选股"""
        syms = ['sh600519','sz300750','sz000858','sh600036','sh601318',
                'sz002594','sh600900','sz300059','sz000333','sh601899',
                'sh600887','sz000568','sh600030','sh600585','sz000651',
                'sh601166','sz002415','sh600690','sz002475','sh600276',
                'sh601012','sz002714','sz000002','sh600809','sz300124']
        return self.get_quotes(syms)

    def get_history(self, symbol, days=200):
        """获取日K线"""
        url = f"http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={symbol},day,,{days}"
        data = self._curl(url)
        if not data:
            return None
        try:
            jd = json.loads(data)
            market = symbol[:2]; code = symbol[2:]
            item = jd.get('data', {}).get(code, {})
            klines = item.get('day') or item.get('qfqday') or []
            if not klines:
                return None
            rows = []
            for r in klines[-days:]:
                if len(r) >= 6:
                    rows.append([r[0], float(r[1]), float(r[2]), float(r[3]), float(r[4]), int(float(r[5]))])
            df = pd.DataFrame(rows, columns=['日期','开盘','收盘','最高','最低','成交量'])
            df['日期'] = pd.to_datetime(df['日期'])
            df['涨跌幅'] = df['收盘'].pct_change() * 100
            df = df.sort_values('日期').reset_index(drop=True)
            return df
        except:
            return None

    def scan_signals(self, symbols=None):
        """扫描技术信号"""
        if not symbols:
            # 自动扫描热门+指数成分
            hot = self.get_hot_stocks()
            symbols = [s['code'] for s in hot[:20]]
        signals = []
        for sym in symbols:
            if sym.startswith('sh') or sym.startswith('sz'):
                # 转格式
                code = sym[2:]
                pre = 'sh' if code.startswith('6') or code.startswith('9') else ('sz' if code.startswith('0') or code.startswith('3') else 'sh')
                full = f"{pre}{code}"
            else:
                full = sym
            hist = self.get_history(full, 100)
            if hist is not None and len(hist) > 20:
                sigs = []
                # MA
                ma5 = hist['收盘'].rolling(5).mean().iloc[-1]
                ma20 = hist['收盘'].rolling(20).mean().iloc[-1]
                price = hist['收盘'].iloc[-1]
                if price > ma5 and ma5 > ma20:
                    sigs.append("📈多头排列")
                elif price < ma5 and ma5 < ma20:
                    sigs.append("📉空头排列")
                elif price > ma20:
                    sigs.append("站上20日线")
                else:
                    sigs.append("20日线下方")

                # RSI
                delta = hist['收盘'].diff()
                gain = delta.clip(lower=0).rolling(14).mean()
                loss = (-delta.clip(upper=0)).rolling(14).mean()
                rsi = 100 - 100 / (1 + gain / loss) if loss.iloc[-1] != 0 else 50
                sigs.append(f"RSI={rsi.iloc[-1]:.0f}")

                signals.append({
                    'code': full,
                    'price': price,
                    'change': round(hist['涨跌幅'].iloc[-1], 2) if '涨跌幅' in hist.columns else 0,
                    'signals': ' | '.join(sigs),
                    'rsi': round(rsi.iloc[-1], 1)
                })
        return signals


def test():
    api = AStockEngine()
    print("=" * 45)

    # 指数
    idx = api.get_index()
    print("📈 大盘指数:")
    for n, d in idx.items():
        a = '🟢' if d['change_pct'] >= 0 else '🔴'
        print(f"  {a} {n}: {d['price']} ({d['change_pct']:+.2f}%)")

    # 热门
    hot = api.get_hot_stocks()
    print("\n🔥 热门股:")
    for s in sorted(hot, key=lambda x: abs(x['change_pct']), reverse=True)[:10]:
        a = '🟢' if s['change_pct'] >= 0 else '🔴'
        print(f"  {a} {s['name']}({s['code']}): {s['price']} ({s['change_pct']:+.2f}%)")

    # 历史
    hist = api.get_history('sh600519', 30)
    if hist is not None:
        print(f"\n📊 茅台最近5日:")
        print(hist.tail(5)[['日期','收盘','涨跌幅']].to_string(index=False))

    # 信号
    print("\n🔍 技术信号扫描:")
    sigs = api.scan_signals()
    for s in sigs[:10]:
        print(f"  {s['code']}: {s['price']} | {s['signals']}")

    print(f"\n{'='*45}")
    print(f"✅ 所有接口测试通过")

if __name__ == "__main__":
    test()


path: finance_toolkit/fourier_analyzer.py
content: """
FFT频谱分析模块 — 供 monitor_v3 调用
对K线序列做傅里叶分析，识别主周期、能量分布
用于辅助策略参数优化
"""
import math

def fft(x):
    """Cooley-Tukey FFT (N=2^k)"""
    n = len(x)
    if n <= 1:
        return x
    # 填充到2的幂
    t = 1
    while t < n:
        t *= 2
    if t != n:
        x = list(x) + [0] * (t - n)
    return _fft_core(x)

def _fft_core(x):
    n = len(x)
    if n <= 1:
        return x
    even = _fft_core(x[0::2])
    odd = _fft_core(x[1::2])
    t = [complex(math.cos(-2*math.pi*k/n), math.sin(-2*math.pi*k/n)) * odd[k] for k in range(n//2)]
    return [even[k] + t[k] for k in range(n//2)] + [even[k] - t[k] for k in range(n//2)]

def detrend(x):
    """去线性趋势"""
    n = len(x)
    if n < 2:
        return x
    mx, my = sum(range(n))/n, sum(x)/n
    xy = sum(i*v for i,v in enumerate(x))
    xx = sum(i*i for i in range(n))
    slope = (xy - n*mx*my) / (xx - n*mx*mx)
    intercept = my - slope*mx
    return [x[i] - (slope*i + intercept) for i in range(n)]

def analyze_spectrum(signal, sample_count=None):
    """
    对信号做FFT并返回频谱分析结果
    
    参数:
        signal: list[float] — 时间序列
        sample_count: int — 采样点数量 (2的幂，默认实际长度)
    
    返回:
        dict: {
            'periods': [(周期天数, 幅度, 能量占比%), ...],  # Top8
            'energy': {'low': 低频占比, 'mid': 中频占比, 'high': 高频占比},
            'dominant_period': 主周期天数,
            'peak_ratio': 主峰/平均幅度比,
            'has_cycle': bool 是否有显著周期,
            'suggestion': str 建议
        }
    """
    n = sample_count or len(signal)
    if n > len(signal):
        n = len(signal)
    
    # 取最近n个点并去趋势
    trimmed = detrend(signal[-n:])
    
    # FFT
    freq = fft(trimmed)
    mags = [abs(v) for v in freq]
    half = len(mags) // 2
    mags = mags[:half]
    
    # 找主周期
    peaks = []
    for i in range(1, len(mags)):
        peaks.append((n / i, mags[i], 0.0))  # (period, amp, energy_pct)
    
    # 能量占比
    total_e = sum(m**2 for m in mags[1:]) or 1
    
    # 按幅度排序
    peaks.sort(key=lambda x: x[1], reverse=True)
    top_peaks = []
    for period, amp, _ in peaks[:8]:
        pct = amp / max(peaks[0][1], 1)  # 相对占比
        top_peaks.append((period, int(amp), round(pct*100, 1)))
    
    # 能量分频
    lo = sum(m**2 for m in mags[1:n//30]) / total_e if n > 30 else 0
    md = sum(m**2 for m in mags[n//30:n//5]) / total_e if n > 5 else 0
    hi = sum(m**2 for m in mags[n//5:]) / total_e
    
    # 判断是否有显著周期
    dom_amp = peaks[0][1] if peaks else 0
    avg_amp = sum(p[1] for p in peaks[1:]) / max(len(peaks)-1, 1)
    ratio = dom_amp / avg_amp if avg_amp else 0
    has_cycle = ratio > 2.5
    dom_period = round(peaks[0][0], 1) if peaks else 0
    
    # 建议
    if has_cycle:
        if dom_period < 10:
            suggestion = "短线操作，关注周期低点"
        elif dom_period < 30:
            suggestion = f"波段操作，按{round(dom_period)}天节奏"
        else:
            suggestion = "中线趋势，注意大级别拐点"
    else:
        suggestion = "频谱分散，趋势跟踪优于周期择时"
    
    return {
        'periods': top_peaks,
        'energy': {
            'low_pct': round(lo*100, 1),    # >30天
            'mid_pct': round(md*100, 1),    # 5-30天
            'high_pct': round(hi*100, 1),   # <5天
        },
        'dominant_period': dom_period,
        'peak_ratio': round(ratio, 1),
        'has_cycle': has_cycle,
        'suggestion': suggestion
    }

def get_strategy_hints(fft_result: dict, score: int) -> str:
    """
    根据频谱分析给出策略调参建议
    """
    hints = []
    e = fft_result['energy']
    
    # 能量分布 -> 操作周期建议
    if e['low_pct'] > 70:
        hints.append(f"长期趋势主导({e['low_pct']}%)，建议中线持仓")
    elif e['mid_pct'] > 30:
        hints.append(f"中频活跃({e['mid_pct']}%)，适合波段操作")
    elif e['high_pct'] > 15:
        hints.append(f"噪声偏高({e['high_pct']}%)，缩短周期参数")
    
    # 策略参数建议
    dom = fft_result.get('dominant_period', 0)
    if dom > 20:
        hints.append(f"MA周期可适当放大到MA{min(round(dom/2), 40)}")
    elif 5 < dom < 20:
        hints.append(f"适合快速MA(MA{round(dom//2)})")
    
    # 如果评分低但有周期
    if score < 20 and fft_result['has_cycle']:
        hints.append("当前评分低但存在周期，可能是调整末端")
    
    return '; '.join(hints) if hints else "频谱正常"

# ===== 直接运行时测试 =====
if __name__ == '__main__':
    # 用新浪接口拿数据测试
    import json, urllib.request
    url = 'http://money.finance.sina.com.cn/quotes_service/api/json_v2.php/CN_MarketData.getKLineData?symbol=sz000009&scale=240&ma=no&datalen=500'
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    resp = urllib.request.urlopen(req, timeout=10)
    data = json.loads(resp.read())
    closes = [float(d["close"]) for d in data]
    
    result = analyze_spectrum(closes, 256)
    print("频谱分析:")
    print(f"  主周期: {result['dominant_period']}天")
    print(f"  显著周期: {'✅' if result['has_cycle'] else '❌'}")
    print(f"  能量分布: 低频{result['energy']['low_pct']}% 中频{result['energy']['mid_pct']}% 高频{result['energy']['high_pct']}%")
    print(f"  Top3: {result['periods'][:3]}")
    print(f"  策略建议: {result['suggestion']}")
    print(f"  调参建议: {get_strategy_hints(result, 30)}")


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal:  | SkillSpector: suspicious (CRITICAL)