# 赵云数据转换指南 - vn.py标准格式

## 🎯 目标
将赵云将军下载的A股数据转换为vn.py标准格式，便于vn.py策略直接使用

## 📋 转换要求

### 1. 目录结构
```
/nas/zhaoyun-data/
├── raw/                    # 原始下载数据（保持原样）
│   ├── daily/             # 原始日线数据
│   ├── financial/         # 财务数据
│   └── stock_info/        # 股票信息
├── processed/             # 处理后的数据
│   └── vnpy_format/       # ✅ vn.py标准格式（新增）
│       ├── daily/         # 日线数据（已转换）
│       │   ├── 2010/      # 按年分区
│       │   ├── 2011/
│       │   └── ...
│       ├── minute/        # 分钟线数据（已转换）
│       └── stock_info/    # 股票信息（已转换）
└── scripts/
    └── convert_to_vnpy.py  # 数据转换脚本（新增）
```

### 2. 数据格式规范

#### 原始数据（赵云格式）
```python
# 假设您的原始数据字段：
{
    'code': '000001',      # 股票代码（可能带交易所前缀）
    'date': '2024-01-01',  # 日期
    'open': 10.5,          # 开盘价
    'high': 11.2,          # 最高价
    'low': 10.3,           # 最低价
    'close': 11.0,         # 收盘价
    'volume': 1000000,     # 成交量（股）
    'amount': 11000000,    # 成交额（元）
    'turnover': 0.5,       # 换手率
}
```

#### 目标格式（vn.py标准）
```python
# vn.py需要的字段：
{
    'symbol': '000001',      # 股票代码（6位，不含交易所）
    'exchange': 'SZ',        # 交易所代码：SH/SZ/BJ
    'datetime': '2024-01-01 15:00:00',  # 时间戳（日线数据用15:00:00）
    'open_price': 10.5,      # 开盘价
    'high_price': 11.2,      # 最高价
    'low_price': 10.3,       # 最低价
    'close_price': 11.0,     # 收盘价
    'volume': 1000000,       # 成交量（股）
    'turnover': 11000000,    # 成交额（元）
    'turnover_rate': 0.5,    # 换手率（可选）
    'interval': '1d',        # 周期：1d=日线，1m=分钟线
}
```

### 3. 转换逻辑

#### 股票代码处理
```python
def parse_symbol(code):
    """
    解析股票代码，返回(symbol, exchange)
    
    示例：
    '000001' -> ('000001', 'SZ')
    '600000' -> ('600000', 'SH')
    '830000' -> ('830000', 'BJ')
    'sh600000' -> ('600000', 'SH')
    'sz000001' -> ('000001', 'SZ')
    """
    # 移除交易所前缀
    if code.startswith('sh'):
        return code[2:], 'SH'
    elif code.startswith('sz'):
        return code[2:], 'SZ'
    elif code.startswith('bj'):
        return code[2:], 'BJ'
    
    # 根据数字判断
    if code.startswith('6'):
        return code, 'SH'
    elif code.startswith(('0', '3')):
        return code, 'SZ'
    elif code.startswith('8'):
        return code, 'BJ'
    
    return code, 'SZ'  # 默认
```

#### 日期时间处理
```python
def format_datetime(date_str, interval='1d'):
    """
    格式化日期时间
    
    日线数据：添加 15:00:00（A股收盘时间）
    分钟线数据：保持原样
    """
    if interval == '1d':
        # 日线数据：YYYY-MM-DD 15:00:00
        return f"{date_str} 15:00:00"
    else:
        # 分钟线数据：保持原样或添加 :00
        if len(date_str) == 10:  # YYYY-MM-DD
            return f"{date_str} 09:30:00"  # 开盘时间
        else:
            return date_str
```

## 🚀 转换脚本示例

### 完整转换脚本：`convert_to_vnpy.py`
```python
#!/usr/bin/env python3
"""
赵云数据 → vn.py格式转换脚本
作者：赵云（数据工程将军）
"""

import pandas as pd
import os
import glob
import logging
from datetime import datetime
from pathlib import Path

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('convert_to_vnpy.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)


class ZhaoyunToVnpyConverter:
    """赵云数据转vn.py格式转换器"""
    
    def __init__(self, raw_data_dir, output_dir):
        """
        初始化转换器
        
        Args:
            raw_data_dir: 赵云原始数据目录
            output_dir: vn.py格式输出目录
        """
        self.raw_dir = raw_data_dir
        self.output_dir = output_dir
        
        # 创建输出目录
        os.makedirs(output_dir, exist_ok=True)
        
    def parse_symbol(self, code):
        """解析股票代码"""
        code_str = str(code)
        
        # 移除交易所前缀
        if code_str.startswith('sh'):
            return code_str[2:8], 'SH'  # 取6位数字
        elif code_str.startswith('sz'):
            return code_str[2:8], 'SZ'
        elif code_str.startswith('bj'):
            return code_str[2:8], 'BJ'
        
        # 根据数字判断
        if code_str.startswith('6'):
            return code_str[:6], 'SH'  # 取前6位
        elif code_str.startswith(('0', '3')):
            return code_str[:6], 'SZ'
        elif code_str.startswith('8'):
            return code_str[:6], 'BJ'
        
        return code_str[:6], 'SZ'  # 默认
    
    def convert_daily_data(self, year=None, limit=None):
        """
        转换日线数据
        
        Args:
            year: 指定年份，None表示所有年份
            limit: 限制转换数量（测试用）
        """
        # 原始数据目录
        raw_daily_dir = os.path.join(self.raw_dir, 'raw/daily')
        if not os.path.exists(raw_daily_dir):
            logger.error(f"原始日线数据目录不存在: {raw_daily_dir}")
            return
        
        # 确定年份
        if year:
            years = [str(year)]
        else:
            years = [d for d in os.listdir(raw_daily_dir) 
                    if os.path.isdir(os.path.join(raw_daily_dir, d))]
            years.sort()
        
        logger.info(f"开始转换日线数据，年份: {years}")
        
        total_converted = 0
        
        for year_dir in years:
            year_path = os.path.join(raw_daily_dir, year_dir)
            output_year_path = os.path.join(self.output_dir, 'daily', year_dir)
            os.makedirs(output_year_path, exist_ok=True)
            
            # 查找所有parquet文件
            parquet_files = glob.glob(os.path.join(year_path, '*.parquet'))
            
            if limit:
                parquet_files = parquet_files[:limit]
            
            logger.info(f"转换 {year_dir} 年数据，共 {len(parquet_files)} 个文件")
            
            for file_idx, file_path in enumerate(parquet_files, 1):
                try:
                    # 读取原始数据
                    df = pd.read_parquet(file_path)
                    
                    # 检查必要字段
                    required = ['date', 'open', 'high', 'low', 'close', 'volume']
                    missing = [f for f in required if f not in df.columns]
                    if missing:
                        logger.warning(f"文件缺少字段: {os.path.basename(file_path)} - {missing}")
                        continue
                    
                    # 从文件名获取股票代码
                    file_name = os.path.basename(file_path)
                    if file_name.startswith('sh'):
                        symbol = file_name[2:8]
                        exchange = 'SH'
                    elif file_name.startswith('sz'):
                        symbol = file_name[2:8]
                        exchange = 'SZ'
                    elif file_name.startswith('bj'):
                        symbol = file_name[2:8]
                        exchange = 'BJ'
                    else:
                        # 尝试从数据中获取
                        if 'code' in df.columns:
                            symbol, exchange = self.parse_symbol(df['code'].iloc[0])
                        else:
                            logger.warning(f"无法确定股票代码: {file_name}")
                            continue
                    
                    # 创建vn.py格式DataFrame
                    vnpy_df = pd.DataFrame()
                    
                    # 基本字段
                    vnpy_df['datetime'] = pd.to_datetime(df['date']).dt.strftime('%Y-%m-%d 15:00:00')
                    vnpy_df['open_price'] = df['open']
                    vnpy_df['high_price'] = df['high']
                    vnpy_df['low_price'] = df['low']
                    vnpy_df['close_price'] = df['close']
                    vnpy_df['volume'] = df['volume']
                    
                    # 可选字段
                    if 'amount' in df.columns:
                        vnpy_df['turnover'] = df['amount']  # 成交额
                    else:
                        vnpy_df['turnover'] = df['volume'] * df['close']  # 估算
                    
                    if 'turnover' in df.columns:
                        vnpy_df['turnover_rate'] = df['turnover']  # 换手率
                    
                    # 标识字段
                    vnpy_df['symbol'] = symbol
                    vnpy_df['exchange'] = exchange
                    vnpy_df['interval'] = '1d'
                    
                    # 保存文件
                    output_file = os.path.join(output_year_path, f"{exchange}{symbol}_daily_vnpy.parquet")
                    vnpy_df.to_parquet(output_file, index=False)
                    
                    total_converted += 1
                    
                    if file_idx % 100 == 0:
                        logger.info(f"进度: {year_dir}年 {file_idx}/{len(parquet_files)}")
                        
                except Exception as e:
                    logger.error(f"转换失败 {file_path}: {e}")
        
        logger.info(f"日线数据转换完成: 共转换 {total_converted} 个文件")
    
    def convert_stock_info(self):
        """转换股票基础信息"""
        raw_stock_dir = os.path.join(self.raw_dir, 'raw/stock_info')
        if not os.path.exists(raw_stock_dir):
            logger.warning(f"股票信息目录不存在: {raw_stock_dir}")
            return
        
        # 查找股票信息文件
        stock_files = glob.glob(os.path.join(raw_stock_dir, '*.parquet')) + \
                     glob.glob(os.path.join(raw_stock_dir, '*.csv'))
        
        if not stock_files:
            logger.warning("未找到股票信息文件")
            return
        
        all_stock_info = []
        
        for file_path in stock_files:
            try:
                # 读取文件
                if file_path.endswith('.parquet'):
                    df = pd.read_parquet(file_path)
                else:
                    df = pd.read_csv(file_path)
                
                # 标准化字段名
                column_mapping = {
                    '代码': 'symbol',
                    '名称': 'name',
                    '行业': 'industry',
                    '市场': 'market',
                    '上市日期': 'list_date',
                    '总市值': 'total_market_cap',
                    '流通市值': 'circulating_market_cap',
                    '市盈率': 'pe',
                    '市净率': 'pb',
                    'ROE': 'roe',
                }
                
                df = df.rename(columns={k: v for k, v in column_mapping.items() if k in df.columns})
                
                # 添加exchange字段
                if 'symbol' in df.columns:
                    df['exchange'] = df['symbol'].apply(lambda x: self.parse_symbol(x)[1])
                
                all_stock_info.append(df)
                logger.info(f"转换股票信息: {os.path.basename(file_path)}")
                
            except Exception as e:
                logger.error(f"转换股票信息失败 {file_path}: {e}")
        
        if all_stock_info:
            # 合并所有数据
            combined_df = pd.concat(all_stock_info, ignore_index=True)
            
            # 去重
            if 'symbol' in combined_df.columns:
                combined_df = combined_df.drop_duplicates(subset=['symbol'])
            
            # 保存
            output_dir = os.path.join(self.output_dir, 'stock_info')
            os.makedirs(output_dir, exist_ok=True)
            
            output_file = os.path.join(output_dir, 'stock_basic_info_vnpy.parquet')
            combined_df.to_parquet(output_file, index=False)
            
            logger.info(f"股票信息转换完成: {output_file} ({len(combined_df)} 只股票)")


def main():
    """主函数"""
    print("=" * 60)
    print("赵云数据 → vn.py格式转换工具")
    print("=" * 60)
    
    # 配置路径
    RAW_DATA_DIR = "/Users/chufeng/nas/stock/sanguo_vnpy/zhaoyun-data/data"
    OUTPUT_DIR = "/Users/chufeng/nas/stock/sanguo_vnpy/zhaoyun-data/processed/vnpy_format"
    
    # 创建转换器
    converter = ZhaoyunToVnpyConverter(RAW_DATA_DIR, OUTPUT_DIR)
    
    # 1. 转换日线数据（测试模式：只转换2024年前10个文件）
    print("\n1. 转换日线数据（测试模式）...")
    converter.convert_daily_data(year=2024, limit=10)
    
    # 2. 转换股票信息
    print("\n2. 转换股票信息...")
    converter.convert_stock_info()
    
    print("\n" + "=" * 60)
    print("转换完成！")
    print(f"输出目录: {OUTPUT_DIR}")
    print("=" * 60)
    print("\n下一步:")
    print("1. 测试转换后的数据")
    print("2. 联系姜维将军集成到vn.py")
    print("3. 批量转换所有年份数据")
    print("=" * 60)


if __name__ == "__main__":
    main()
```

## 📋 转换工作清单

### 第一阶段：测试转换（立即开始）
1. ✅ 创建转换脚本 `convert_to_vnpy.py`
2. ✅ 测试转换2024年的前10个股票文件
3. ✅ 验证转换后的数据格式
4. ✅ 调整转换逻辑（如有问题）

### 第二阶段：批量转换（测试通过后）
1. 🔄 转换所有年份的日线数据
2. 🔄 转换股票基础信息
3. 🔄 转换财务数据（可选）
4. 🔄 转换分钟线数据（可选）

### 第三阶段：定期更新
1. 🔄 设置定时任务，自动转换新数据
2. 🔄 数据质量监控
3. 🔄 版本管理

## 🔧 技术要点

### 1. 性能优化
- 使用pandas批量处理
- 按年分区存储
- Parquet格式压缩存储

### 2. 数据质量
- 字段完整性检查
- 数据有效性验证
- 异常值处理

### 3. 可维护性
- 详细的日志记录
- 错误重试机制
- 配置化参数

## 📞 协作流程

### 数据更新流程：
```
赵云：下载新数据 → 运行转换脚本 → 更新vnpy_format目录 → 通知姜维
姜维：测试新数据 → 更新适配器配置 → 部署到生产环境
```

### 问题处理：
- **数据问题**：联系赵云将军
- **格式问题**：参考本指南调整转换逻辑
- **集成问题**：联系姜维将军

---

**赵云将军，请按此指南开始数据转换工作。转换完成后，末将（姜维）立即开始vn.py集成工作！**

**分工明确，协同作战，粮道必通！** 🚛

**姜维 - 后勤总督**
*2026-03-29*