Home >  > 期货数据处理(含复权)

期货数据处理(含复权)

数据处理的流程:
1.对数据进行复权(只保留 2016-01-01(含)之后的数据)。
2.验证复权的正确性。
3.由于vnpy的symbol格式是RB999.SHFE这样的格式,所以建立一个期货代码与交易所关系的symbol.json
4.写入SQLite数据库。

最后只要复制 write_to_db.py、symbol.json这2个文件到期货1分钟数据的目录下,运行,它就可以将数据自动复权、并写入到SQLite数据库。

一、对没有复权的主连合约进行复权

我用的是加减复权,代码(并写入数据库write_to_db.py)

import pandas as pd
import sqlite3  # 新增导入


from pathlib import Path

def get_file_prefixes(directory=".", recursive=False, prefix_mode="stem", sep=None, length=None, extensions=None, return_path=False):
    """
    遍历目录下的所有文件,提取文件名前缀。

    参数:
        directory: 目标目录,默认为当前目录 (".")
        recursive: 是否递归子目录,默认 False(仅当前目录)
        prefix_mode: 前缀提取模式
            - "stem": 返回不带扩展名的文件名(如 "file.txt" -> "file")
            - "split": 按分隔符 sep 分割,取第一部分(如 "data_2026.csv" -> "data")
            - "length": 取文件名的前 length 个字符(不包含扩展名,若 length 指定)
        sep: 当 mode="split" 时使用的分隔符,默认为 "_"
        length: 当 mode="length" 时截取的长度
        extensions: 指定要处理的文件扩展名元组,如 ('.csv', '.txt'),默认为 ('.csv',)
        return_path: 是否返回完整路径,默认为 False(仅返回前缀字符串列表)

    返回:
        若 return_path=False: 返回前缀字符串列表(如 ['PT', 'data'])
        若 return_path=True:  返回 (文件路径, 前缀) 元组列表
    """
    if extensions is None:
        extensions = ('.csv',)
    extensions = tuple(ext.lower() for ext in extensions)

    base = Path(directory)
    if not base.exists() or not base.is_dir():
        raise ValueError(f"目录不存在或不是文件夹: {directory}")

    file_iter = base.rglob("*") if recursive else base.glob("*")

    result = []
    for item in file_iter:
        if item.is_file() and item.suffix.lower() in extensions:
            name = item.name

            if prefix_mode == "stem":
                prefix = item.stem
            elif prefix_mode == "split":
                split_char = sep if sep is not None else "_"
                prefix = name.split(split_char)[0] if split_char in name else name
            elif prefix_mode == "length":
                stem = item.stem
                prefix = stem[:length] if length is not None else stem
            else:
                raise ValueError("prefix_mode 必须是 'stem', 'split' 或 'length'")

            if return_path:
                result.append((str(item), prefix))
            else:
                result.append(prefix)

    return result[0]


import json
def find_exchange_from_json(symbol, json_file='symbol.json'):
    with open(json_file, 'r', encoding='utf-8') as f:
        mapping = json.load(f)
    for exchange, symbols in mapping.items():
        if symbol in symbols:
            return exchange
    return None



# ---------- 1. 读取 ----------
file_prefix = get_file_prefixes
df = pd.read_csv(file_prefix+'.csv', parse_dates=['datetime'])
# 只保留 2016-01-01(含)之后的数据
df = df[df['datetime'] >= '2016-01-01']
df.sort_values('datetime', inplace=True)
df.reset_index(drop=True, inplace=True)

# ---------- 2. 保存原始收盘价 ----------
df['close_raw'] = df['close']

# ---------- 3. 提取关键列为列表(脱离DataFrame) ----------
symbols = df['symbol'].tolist()
opens   = df['open'].tolist()
highs   = df['high'].tolist()
lows    = df['low'].tolist()
closes  = df['close'].tolist()
close_raws = df['close_raw'].tolist()

# ---------- 4. 等差后复权 ----------
cum_adj = 0.0
last_symbol = None
last_close_raw = None

new_opens  = []
new_highs  = []
new_lows   = []
new_closes = []

for i in range(len(df)):
    sym = symbols[i]
    if last_symbol is not None and sym != last_symbol:
        # 换月跳空差值 = 旧合约最后收盘 - 新合约第一开盘
        delta = last_close_raw - opens[i]
        cum_adj += delta

    # 应用累积因子
    new_opens.append(opens[i] + cum_adj)
    new_highs.append(highs[i] + cum_adj)
    new_lows.append(lows[i] + cum_adj)
    new_closes.append(closes[i] + cum_adj)

    # 更新状态(用的是原始收盘价,即 close_raws[i])
    last_symbol = sym
    last_close_raw = close_raws[i]

# ---------- 5. 覆盖原列 ----------
df['open']  = new_opens
df['high']  = new_highs
df['low']   = new_lows
df['close'] = new_closes

# ---------- 6. 保存 ----------
#df.to_csv('rb_result.csv', index=False)
#print("复权完成,结果已保存至 rb_result.csv")
# ---------- 6. 保存 ----------
# 获取期货商品编码(前缀)
symbol = file_prefix + '999'       # 拼接“999”作为 symbol

# 查找交易所
exchange = find_exchange_from_json(file_prefix)

# 构建待入库的 DataFrame,字段名与数据库表完全对应
df_db = pd.DataFrame({
    'symbol': symbol,                     # 统一为 RB999
    'exchange': exchange,                 # 从 json 映射获得
    'datetime': df['datetime'],           # 直接复制
    'interval': '1m',                     # 固定为 1m
    'volume': df['volume'],
    'turnover': df['amount'],
    'open_interest': df['position'],
    'open_price': df['open'],
    'high_price': df['high'],
    'low_price': df['low'],
    'close_price': df['close']
})

# 连接数据库(路径固定)
db_path = r'C:\Users\Administrator\.vntrader\database.db'
conn = sqlite3.connect(db_path)

# 追加写入(表不存在时自动创建)
df_db.to_sql('dbbardata', conn, if_exists='append', index=False)

conn.close()
print("复权完成,数据已保存至 database.db 的 dbbardata 表")

验证1:

在3.11 21:00的时候,开盘价是2000,将它复权成前一分钟的收盘价2085,所以都要加上85,复权后的数据变成这样。

验证2:
在8/17/2016 21:00的时候,开盘价是2531,将它复权成前一分钟的收盘价2589,要加上58.

核心逻辑:没有问题 ✅

    换月判定(sym != last_symbol)✅
    delta 用原始收盘/开盘计算、cum_adj 单调累加 ✅(数学自洽)
    换月后整段平移、volume/amount/position 不动 ✅(只有价格需复权)
    样例里 JM1309 只出现 2013-06-28 一段、JM1401 只出现 2013-07-01 起一段——我之前担心的两个隐患都不触发:

✅ 无"合约全历史拼接"(新合约换月前没有历史行需平移)
✅ 无同 symbol 重复段(不会重复调整)

一个需要你决策的权衡点(不是 bug,但影响回测)
任何对齐方式都无法让 open 和 close 同时连续,必须二选一:

开盘对齐(你现在的):开盘价连续,但换月那分钟的 close-to-close 收益有 +0.2% 假跳空;
收盘对齐:收盘连续,换月第一分钟 open 有假跳空。

你的数据是分钟级,如果策略按分钟收益(close/close-1)或把分钟聚合成日线算日收益,换月那分钟/那天的收益会被 +0.2% 假跳空污染。建议根据你的策略用哪种价格确认对齐方式——不是代码错,是设计要选对。
一个方法固有的警告(长周期尤其要注意)

等差后复权对13 年分钟级数据(2013→2026)会累积巨大的 delta,历史价格水平被严重平移(比如 2013 年的 1000 元可能被调成 1800 元)。分钟/日收益率不受影响(同一合约内加同一常数,差分抵消),但所有依赖绝对价格的逻辑会坏:固定止损价、挂单价、ATR 百分比、涨跌停判断等。如果你只用收益率做因子,可以接受;如果策略里有价格阈值,建议用比例后复权对比验证。

附对csv数据进行指定查询的代码:

import pandas as pd

# ---------- 1. 读取数据 ----------
df = pd.read_csv('RB.csv', parse_dates=['datetime'])
df.sort_values('datetime', inplace=True)
df.set_index('datetime', inplace=True)

# ---------- 2. 查询函数(前后各 5 条) ----------
def get_record_with_neighbors(time_str, n=5):
    """
    根据时间字符串返回该行及其前后各 n 行(默认 n=5)。
    若时间不在数据中,则提示并返回 None。
    """
    try:
        target = pd.to_datetime(time_str)
    except Exception:
        print("输入的时间格式无效,请使用如 '1/4/2016 9:09' 的格式。")
        return None

    if target not in df.index:
        print(f"数据中不存在时间为 {time_str} 的记录。")
        return None

    idx = df.index.get_loc(target)          # 目标在排序后索引中的位置(整数)
    start = max(0, idx - n)                 # 向前 n 条,但不小于 0
    end = min(len(df) - 1, idx + n)         # 向后 n 条,但不超出总行数

    result = df.iloc[start:end+1]           # 切片左闭右闭
    return result

# ---------- 3. 交互示例 ----------
if __name__ == "__main__":
    user_input = input("请输入时间(如 1/4/2016 9:09):")
    output = get_record_with_neighbors(user_input, n=5)
    if output is not None:
        print(f"\n查询结果(包含前 5 条、目标、后 5 条):")
        print(output)

如何自己进行处权处理?
https://www.zhihu.com/question/276668028/answer/3262165543
https://www.vnpy.com/forum/topic/34756-ctaliang-hua-tong-guan-xi-lie-3-lian-xu-he-yue-mei-gao-dui-hui-ce-jie-lun-kao-bu-zhu

二、期货代码与交易所的关系symbol.json

{
    "CFFEX": [
        "IC",
        "IF",
        "IH",
        "IM",
        "T",
        "TF",
        "TL",
        "TS"
    ],
    "CZCE": [
        "AP",
        "CF"
    ],
}

symbol

三、提取文件名的前缀



from pathlib import Path

def get_file_prefixes(directory=".", recursive=False, prefix_mode="stem", sep=None, length=None, extensions=None, return_path=False):
    """
    遍历目录下的所有文件,提取文件名前缀。

    参数:
        directory: 目标目录,默认为当前目录 (".")
        recursive: 是否递归子目录,默认 False(仅当前目录)
        prefix_mode: 前缀提取模式
            - "stem": 返回不带扩展名的文件名(如 "file.txt" -> "file")
            - "split": 按分隔符 sep 分割,取第一部分(如 "data_2026.csv" -> "data")
            - "length": 取文件名的前 length 个字符(不包含扩展名,若 length 指定)
        sep: 当 mode="split" 时使用的分隔符,默认为 "_"
        length: 当 mode="length" 时截取的长度
        extensions: 指定要处理的文件扩展名元组,如 ('.csv', '.txt'),默认为 ('.csv',)
        return_path: 是否返回完整路径,默认为 False(仅返回前缀字符串列表)

    返回:
        若 return_path=False: 返回前缀字符串列表(如 ['PT', 'data'])
        若 return_path=True:  返回 (文件路径, 前缀) 元组列表
    """
    if extensions is None:
        extensions = ('.csv',)
    extensions = tuple(ext.lower() for ext in extensions)

    base = Path(directory)
    if not base.exists() or not base.is_dir():
        raise ValueError(f"目录不存在或不是文件夹: {directory}")

    file_iter = base.rglob("*") if recursive else base.glob("*")

    result = []
    for item in file_iter:
        if item.is_file() and item.suffix.lower() in extensions:
            name = item.name

            if prefix_mode == "stem":
                prefix = item.stem
            elif prefix_mode == "split":
                split_char = sep if sep is not None else "_"
                prefix = name.split(split_char)[0] if split_char in name else name
            elif prefix_mode == "length":
                stem = item.stem
                prefix = stem[:length] if length is not None else stem
            else:
                raise ValueError("prefix_mode 必须是 'stem', 'split' 或 'length'")

            if return_path:
                result.append((str(item), prefix))
            else:
                result.append(prefix)

    return result[0]

# 示例1:只取当前目录下的文件,返回去掉扩展名的前缀(最常用)
prefixes = get_file_prefixes(directory=".", recursive=False, prefix_mode="stem")

print(prefixes)




import json
def find_exchange_from_json(symbol, json_file='symbol.json'):
    with open(json_file, 'r', encoding='utf-8') as f:
        mapping = json.load(f)
    for exchange, symbols in mapping.items():
        if symbol in symbols:
            return exchange
    return None


a = find_exchange_from_json(prefixes)
print(a)







三、读取vnpy database.db,并划k线
输入起止日期。
经核对,MA999与我Twenty02Strategy策略中生成的日线一致。

import sqlite3
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from datetime import datetime
import sys

def fetch_daily_kline_interactive(db_path, symbol, start_date, end_date, table_name='dbbardata'):
    """
    从数据库读取1分钟数据,合成日K(夜盘归属次日,周五夜盘归下周一),使用Plotly绘制交互式K线图
    """
    conn = sqlite3.connect(db_path)
    
    # 探测时间字段类型
    cursor = conn.cursor()
    cursor.execute(f"SELECT datetime FROM {table_name} LIMIT 1;")
    sample = cursor.fetchone()
    if sample is None:
        conn.close()
        raise ValueError(f"表 {table_name} 中无数据。")
    
    time_sample = sample[0]
    is_timestamp = isinstance(time_sample, (int, float))
    
    if is_timestamp:
        time_condition = "date(datetime, 'unixepoch') BETWEEN ? AND ?"
    else:
        time_condition = "date(datetime) BETWEEN ? AND ?"
    
    query = f"""
    SELECT *
    FROM {table_name}
    WHERE symbol = ? AND interval = '1m'
      AND {time_condition}
    ORDER BY datetime ASC
    """
    df = pd.read_sql_query(query, conn, params=(symbol, start_date, end_date))
    conn.close()

    if df.empty:
        print(f"在 {start_date} ~ {end_date} 期间未找到 {symbol} 的1分钟数据。")
        return

    # 时间处理
    if is_timestamp:
        df['datetime'] = pd.to_datetime(df['datetime'], unit='s')
    else:
        df['datetime'] = pd.to_datetime(df['datetime'])
    df.set_index('datetime', inplace=True)

    # ====== 自定义交易日归属(夜盘归属次日,周五夜盘归属下周一)=======
    def get_trade_date(dt):
        date_ = dt.date()
        if dt.hour < 21:
            # 白天时段:归属当天
            return date_
        else:
            # 夜盘时段:判断是否为周五
            if date_.weekday() == 4:  # 周五
                return date_ + pd.Timedelta(days=3)  # 下周一
            else:
                return date_ + pd.Timedelta(days=1)  # 次日

    df['trade_date'] = df.index.map(get_trade_date)

    # 按交易日聚合
    ohlc_dict = {
        'open_price': 'first',
        'high_price': 'max',
        'low_price': 'min',
        'close_price': 'last',
        'volume': 'sum'
    }
    exist_cols = [col for col in ohlc_dict.keys() if col in df.columns]
    ohlc_dict = {col: ohlc_dict[col] for col in exist_cols}

    daily = df.groupby('trade_date').agg(ohlc_dict).dropna()
    daily.reset_index(inplace=True)               # trade_date 变为列
    daily.rename(columns={'trade_date': 'datetime'}, inplace=True)  # 绘图时使用

    # 构建交互式图表(主图K线 + 副图成交量)
    fig = make_subplots(
        rows=2, cols=1,
        shared_xaxes=True,
        vertical_spacing=0.03,
        row_heights=[0.7, 0.3]
    )

    # 主图:K线
    fig.add_trace(
        go.Candlestick(
            x=daily['datetime'],
            open=daily['open_price'],
            high=daily['high_price'],
            low=daily['low_price'],
            close=daily['close_price'],
            name='日K线',
            showlegend=True
        ),
        row=1, col=1
    )

    # 副图:成交量(涨红跌绿)
    colors = ['red' if daily['close_price'].iloc[i] < daily['open_price'].iloc[i] 
              else 'green' for i in range(len(daily))]
    fig.add_trace(
        go.Bar(
            x=daily['datetime'],
            y=daily['volume'],
            name='成交量',
            marker_color=colors,
            showlegend=True
        ),
        row=2, col=1
    )

    # 布局设置
    fig.update_layout(
        title=f'{symbol} 日K线(夜盘归属次日,周五夜盘归下周一) ({start_date} ~ {end_date})',
        xaxis_rangeslider_visible=False,
        template='plotly_dark',
        height=700,
        hovermode='x unified'
    )

    config = {
        'scrollZoom': True,
        'displayModeBar': True,
        'modeBarButtonsToRemove': ['toImage']
    }

    fig.show(config=config)
    return daily


if __name__ == "__main__":
    try:
        start = input("请输入开始日期 (YYYY-MM-DD): ").strip()
        end = input("请输入结束日期 (YYYY-MM-DD): ").strip()
        datetime.strptime(start, "%Y-%m-%d")
        datetime.strptime(end, "%Y-%m-%d")
    except ValueError:
        print("日期格式错误,请使用 YYYY-MM-DD 格式。")
        sys.exit(1)

    fetch_daily_kline_interactive('database.db', 'MA999', start, end)

四、deepseek harness用1分钟k合成日k
合成的数据与我自己写的合成的完全一致。

暧昧帖

本文暂无标签