VN.PY 2.0学习记录一(如何回测)

VNPY 12 浏览 9 分钟阅读

蜗牛博客VNPY学习记录:
VN.PY 2.0学习记录一(如何回测)
VN.PY 2.0学习记录二(策略开发)
Vn.py学习记录三(米筐教程)
VN.PY 2.0学习记录四(多线程、多进程)
Vn.py学习记录五–交易时间段及Widgets
Vn.py学习记录六(无界面模拟盘)
Vn.py学习记录七(V2.0.5版本)
Vnpy学习记录八(R-Breaker及pickle)
Vn.py学习记录九(事件驱动引擎)
VN.PY学习记录十(源码概述)
VNPY学习记录11(微信+Vscode)
VNPY学习记录12(父子进程、回调函数)
VNPY学习记录13(部署到云服务器,实现自动交易)

说明:
使用界面回测时,每次修改了策略,要重新登陆vnpy,非常麻烦。

一、通过getdata.py获取回测数据
现在VNPY与米筐合作,可以非常方便地获得相关数据。

1.填写下载账号
这里又有一个坑,首先说明一下,下载了VN.PY的安装文件,解压缩之后,会有一个VNPY的文件夹,然后执行了安装程序之后,会在C:\ProgramData\VNConda\Lib\site-packages\下面也生成一个VNPY的文件夹。

我之前说过,写策略需要在C:\ProgramData\VNConda\Lib\site-packages\vnpy\app\cta_strategy\strategies这个文件夹下面建立,不然根本无法加载。然后今天打算测试回测的时候,因为记得之前看教程说需要在一个文件中输入ricqquant的账号与密码,所以我一个劲地在C:\ProgramData\VNConda\Lib\site-packages\vnpy\这个文件夹下面找文件,找了一通没有找到。后来竟然在F:\vnpy\vnpy-2.0.2\tests\backtesting\这里面找到了,要修改的其实就是getdata.py文件!

打开getdata.py文件,填写账号与密码。

注意:
填写代码时要注意大小写,比如螺纹钢要写“RB88.SHFE”才能下载到数据,用rb88.SHFE就不行。

2.获取数据

执行getdata.py文件,然后“咚”的一声,又掉坑里了。出现如下的错误提示:

rqdatac.share.errors.AuthenticationFailed: authentication failed.

不知是因为现在是五一假期非交易时间段,还是账号的原因,反正就是取不到数。

Updated on Jul.06 2019:

1.这里经后来确认,是由于我输错了账号、密码的原因。米筐的账号、密码是make.bat里面的那个,不是注册网站的那个。

2.这个getdata文件随便放哪儿都可以。

只要填写了正确的RQdata账号与密码,那么就可以取到数据了:

取到的数据是直接写入到了sqlite数据库,比如我设定的是取20190501-20190601的数据,这里就已经取到了。

导入的数据在哪儿呢?
默认生成的databased.db文件在当前运行目录下的.vntrader下,我的是在c盘user目录下的.vntrader里面。可以使用SQLiteSPY查看:

二、自己写代码获取数据
其实这一部分完全是自己最开始使用的时候,无法通过getdata.py文件获取数据折腾出来的,当作自己的笔记放这儿吧,大家如果是通过其他途径获的数据,可以将这个作为参考如何导入数据。

(一)安装jupyter
安装好VN.py2.0.2之后,jupyter是用不了的,需要用下面的命令重新安装。

pip install jupyter

(二)试用ricequant

1.RQDatac

pip install --extra-index-url https://rquser:ricequant99@py.ricequant.com/simple/ rqdatac

2.配置License
直接执行ricequant提供的make.bat文件即可,然后重启CMD窗口。

3.取tick数据
新建quant.py文件,代码如下:

import rqdatac as rq
from rqdatac import *
rq.init()

rq.get_price('000001.XSHE','2018-3-23','2018-3-23','tick')

成果展示:

4.取股票数据

import rqdatac as rq
import pandas as pd
from rqdatac import *
rq.init()

data = rq.get_price('平安银行','2018-3-23','2018-4-23')
df = pd.DataFrame(data)
df.to_csv('pingan.csv')

成果展示:

备注:
默认取一天的数据,如果要取一分钟的数据则用:

data = rq.get_price('平安银行','2018-3-23','2018-3-28','1m')

函数的详细用法看这里:
https://www.ricequant.com/doc/rqdata-institutional#research-API-get_price

5.获取1分钟数据

import rqdatac as rq
import pandas as pd
from rqdatac import *
rq.init()

FIELDS = ["open", "high", "low", "close", "volume"]

data = rq.get_price(
    "IF88",
    frequency="1m",
    fields=FIELDS,
    start_date='20190101',
    end_date='20190630'
)

df = pd.DataFrame(data)
df.to_csv('cf88new.csv')

(三)载入CSV文件
1.按下面的格式整理一份CSV文件,

2.导入数据:
执行VNPY的“功能————CSV导入”功能。

这里又有一个小坑,VN.PY默认的表头信息是“Open,High,Low,Close”这样第一个字母大写的形式,而从ricequant导出的数据是“open,high,low,close”这样第一个不是大写的,当我第一次运行的时候,明明所有字段都错了,却只提示我“volume”这个字段出错,结果我就在那里找呀找呀,找了半天没找到到底哪里出错了,后来才发现是这个字段首字母大小写的问题。

3.导入成功:

备注:
上面整理数据的那一步其实可以省略,设定好字段后,程序会自动识别。
另外,第二次导入数据之后,会在第一次的数据后面添加数据。

4.查看数据

三、策略回测
(一)回测方法一
使用vnpy的UI界面进行回测,这个简单,就不说了。
(二)自己写代码回测
新建一个q2.py文件用于回测,比如我的就放在C:\Users\Administrator\下面。

备注:回测的策略必须放在:C:\veighna_studio\Lib\site-packages\vnpy_ctastrategy\strategies 下面。

写回测代码:

from vnpy.app.cta_strategy.backtesting import BacktestingEngine
from vnpy.app.cta_strategy.strategies.boll_channel_strategy import BollChannelStrategy
from datetime import datetime

engine = BacktestingEngine()
engine.set_parameters(
	vt_symbol = "000001.SZSE",
	interval ="d",
	start = datetime(2018,3,23),
	end = datetime(2018,4,23),
	rate = 0,
	slippage = 0,
	size = 10,
	pricetick = 0.2,
	capital = 1_000_000,
	)

engine.add_strategy(BollChannelStrategy,{})
engine.load_data()
engine.run_backtesting()
df = engine.calculate_result()
engine.calculate_statistics()
engine.show_chart()

执行q2.py成果展示:

Updated on Aug.6.2026
更新了,直接读取csv文件回测。

from datetime import datetime
import pandas as pd

from vnpy_ctastrategy.backtesting import BacktestingEngine
from vnpy_ctastrategy.strategies.double_ma_strategy import DoubleMaStrategy
from vnpy.trader.constant import Exchange, Interval
from vnpy.trader.object import BarData


def load_bars_from_csv(
    filepath: str,
    symbol: str,
    exchange: Exchange
) -> list[BarData]:
    """从CSV文件加载K线数据,转换为BarData列表"""
    df: pd.DataFrame = pd.read_csv(filepath)          # 修正:使用传入的文件路径
    bars: list[BarData] = []

    for row in df.itertuples():
        bar: BarData = BarData(
            symbol=symbol,
            exchange=exchange,
            datetime=datetime.strptime(row.datetime, "%m/%d/%Y %H:%M"),  # 修正时间格式
            interval=Interval.MINUTE,
            open_price=row.open,
            high_price=row.high,
            low_price=row.low,
            close_price=row.close,
            volume=row.volume,
            gateway_name="CSV"
        )
        bars.append(bar)

    return bars


engine = BacktestingEngine()
engine.set_parameters(
    vt_symbol="RB888.SHFE",
    interval=Interval.MINUTE,
    start=datetime(2018, 3, 23),
    end=datetime(2018, 6, 23),
    rate=0,
    slippage=0,
    size=10,
    pricetick=0.2,
    capital=1_000_000,
)

engine.add_strategy(DoubleMaStrategy, {})
# engine.load_data()

# 跳过 load_data(),直接注入 CSV 数据
engine.history_data = load_bars_from_csv(
    "test.csv", "rb888", Exchange.SHFE
)

engine.run_backtesting()
df = engine.calculate_result()
engine.calculate_statistics()
engine.show_chart()

# 假设 engine 是你已经运行完毕的 BacktestingEngine 实例
trades = engine.get_all_trades()      # 返回 List[TradeData]
#保存成交记当。同样的,每日盈亏可以用 engine.get_all_daily_results(),持仓记录用 engine.get_all_positions() 等。
df2 = pd.DataFrame([t.__dict__ for t in trades])
df2.to_csv('trade_log.csv', index=False)

四、一个策略多品种批量回测
效果展示:

from datetime import datetime
import pandas as pd

from vnpy_ctastrategy.backtesting import BacktestingEngine
from vnpy_ctastrategy.strategies.twenty03_strategy import Twenty03Strategy
from vnpy.trader.constant import Interval

# 定义需要回测的品种列表,每个品种包含合约代码、乘数、最小变动价位
symbols = [
    {"vt_symbol": "TA999.CZCE", "size": 5,  "pricetick": 2},   # PTA:5吨/手,2元/吨
    {"vt_symbol": "SA999.CZCE", "size": 20, "pricetick": 1},   # 纯碱:20吨/手,1元/吨
    {"vt_symbol": "P999.DCE",   "size": 10, "pricetick": 2},   # 棕榈油:10吨/手,2元/吨
    {"vt_symbol": "FU999.SHFE", "size": 10, "pricetick": 1},   # 燃料油:10吨/手,1元/吨
    {"vt_symbol": "V999.DCE",   "size": 5,  "pricetick": 1},   # PVC:5吨/手,1元/吨
    {"vt_symbol": "RB999.SHFE", "size": 10, "pricetick": 1},   # 螺纹钢:10吨/手,1元/吨
    {"vt_symbol": "MA999.CZCE", "size": 10, "pricetick": 1},   # 甲醇:10吨/手,1元/吨
    {"vt_symbol": "AL999.SHFE", "size": 5,  "pricetick": 5},   # 铝:5吨/手,5元/吨
    {"vt_symbol": "SP999.SHFE", "size": 10, "pricetick": 2},   # 纸浆:10吨/手,2元/吨
    {"vt_symbol": "FG999.CZCE", "size": 20, "pricetick": 1},   # 玻璃:20吨/手,1元/吨
    {"vt_symbol": "AU999.SHFE", "size": 1000, "pricetick": 0.02},   # 沪金:1000克/手,最小价位:0.02
    {"vt_symbol": "AG999.SHFE", "size": 15, "pricetick": 1},   # 沪银:15千克/手,最小价位:1
    {"vt_symbol": "RU999.SHFE", "size": 10, "pricetick": 5},   # 天然橡胶:10吨/手,最小价位:5
    {"vt_symbol": "SR999.CZCE", "size": 10, "pricetick": 1},   # 白糖:10吨/手,最小价位:1
    {"vt_symbol": "M999.DCE", "size": 10, "pricetick": 1},   # 豆粕:10吨/手,最小价位:1
    {"vt_symbol": "I999.DCE", "size": 100, "pricetick": 0.5},   # 铁矿石:100吨/手,最小价位:0.5
    {"vt_symbol": "J999.DCE", "size": 100, "pricetick": 0.5},   # 焦炭:100吨/手,最小价位:0.5
    {"vt_symbol": "JM999.DCE", "size": 60, "pricetick": 0.5},   # 焦煤:60吨/手,最小价位:0.5
    {"vt_symbol": "SC999.INE", "size": 1000, "pricetick": 0.1},   # 原油:1000桶/手,最小价位:0.1
    {"vt_symbol": "CU999.SHFE", "size": 5, "pricetick": 10},   # 沪铜:5吨/手,最小价位:10   
    {"vt_symbol": "NI999.SHFE", "size": 1, "pricetick": 10},   # 沪镍:1吨/手,最小价位:10  
    {"vt_symbol": "SN999.SHFE", "size": 1, "pricetick": 10},   # 沪锡:1吨/手,最小价位:10 
    {"vt_symbol": "RM999.CZCE", "size": 10, "pricetick": 1},   # 菜粕:10吨/手,最小价位:1        
    {"vt_symbol": "CF999.CZCE", "size": 5, "pricetick": 5},   # 棉花:5吨/手,最小价位:5 
]

# 公共回测参数
start = datetime(2016, 1, 1)
end = datetime(2026, 7, 31)
interval = Interval.MINUTE
rate = 0.0001                #万分之一手续费 
slippage = 1                 #1个最小变动价位滑点 
capital = 100_000

# 用于存储每个品种的回测结果摘要
results_summary = []
# 用于存储所有品种每年的收益
yearly_summary = []

for sym in symbols:
    # 为每个品种创建独立的回测引擎
    engine = BacktestingEngine()
    engine.set_parameters(
        vt_symbol=sym["vt_symbol"],
        interval=interval,
        start=start,
        end=end,
        rate=rate,
        slippage=slippage,
        size=sym["size"],
        pricetick=sym["pricetick"],
        capital=capital,
    )

    # 添加策略(所有品种使用相同策略参数)
    engine.add_strategy(Twenty03Strategy, {})

    # 加载数据并运行回测
    engine.load_data()
    engine.run_backtesting()

    # 计算回测结果
    df = engine.calculate_result()
    stats = engine.calculate_statistics()

    # 方法1:从 stats 中获取百分比收益率,并正确计算期末资金
    total_return = stats.get("total_return", 0)      # 已是百分比数值
    final_balance = capital * (1 + total_return / 100)

    # 方法2(更准确):直接从回测结果 DataFrame 取最后一天的权益
    # final_balance = df["balance"].iloc[-1]

    annual_return = stats.get("annual_return", 0)
    max_drawdown = stats.get("max_drawdown", 0)

    results_summary.append({
        "vt_symbol": sym["vt_symbol"],
        "final_balance": final_balance,
        "total_return(%)": total_return,       # 建议列名注明百分比
        "annual_return(%)": annual_return,
        "max_drawdown": max_drawdown,
    })

    # ========== 新增:计算并记录每年收益 ==========
    if df is not None and not df.empty:
        # 确保索引为DatetimeIndex
        if not isinstance(df.index, pd.DatetimeIndex):
            df.index = pd.to_datetime(df.index)

        # 按年份分组,取每年最后一个交易日的权益
        yearly_balance = df.groupby(df.index.year)['balance'].last()

        prev_balance = capital
        print(f"\n===== {sym['vt_symbol']} 年度收益率 =====")
        for year, end_balance in yearly_balance.items():
            ret = (end_balance - prev_balance) / prev_balance * 100
            yearly_summary.append({
                'vt_symbol': sym['vt_symbol'],
                'year': year,
                'return(%)': round(ret, 2)
            })
            print(f"{year}: {ret:.2f}%")
            prev_balance = end_balance
    # ========== 年度收益计算结束 ==========

    # ========== 导出成交记录(原有代码) ==========
    trades = engine.get_all_trades()
    if trades:
        # 将 TradeData 对象列表转换为 DataFrame
        df_trades = pd.DataFrame([t.__dict__ for t in trades])
        # 生成安全的文件名(替换可能存在的路径分隔符)
        safe_symbol = sym["vt_symbol"].replace("/", "_").replace(":", "_")
        trade_filename = f"{safe_symbol}_trades.csv"
        df_trades.to_csv(trade_filename, index=False)
        print(f"已保存 {sym['vt_symbol']} 的成交记录至 {trade_filename},共 {len(trades)} 条")
    else:
        print(f"{sym['vt_symbol']} 没有成交记录")
    # ========== 成交记录导出结束 ==========

    # 可选:显示单个品种的图表(若需要可取消注释)
    #engine.show_chart()

# 将汇总结果转为 DataFrame 并打印
summary_df = pd.DataFrame(results_summary)
print("\n===== 各品种回测汇总 =====")
print(summary_df)

# 保存汇总结果到 CSV
summary_df.to_csv("backtest_summary.csv", index=False)

# 打印并保存年度收益汇总
if yearly_summary:
    yearly_df = pd.DataFrame(yearly_summary)
    print("\n===== 所有品种年度收益率汇总 =====")
    print(yearly_df)
    yearly_df.to_csv("yearly_returns.csv", index=False)






五、解决问题
上面的回测虽然运行成功,可是没有任何成交数据。到底是什么原因呢?代码问题?数据导入的时候的格式问题?还是其他的问题。

后来通过测试发现是数据量的问题,我用来测试的数据量太少了,后来将测试的1分钟数据期间改为2个月,终于好了。
成果展示:

图形展示:

用两个月数据大致测试了一下各个策略的年化收益率:
AtrRsiStrategy 16.88%

BollChannelStrategy 21.75%

DoubleMaStrategy -222.75%

DualThrustStrategy 48.12%

KingKeltnerStrategy 报错

MultiSignalStrategy -279%

MultiTimeframeStrategy 18.27%

TestStrategy 报错

TurtleSignalStrategy 134.98

看一个IF88.CFFEX从2016,1,1到2019,7,1的策略,有的年份很赚, 有的年份还会亏钱:

1

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章