跳转至

基于 qstock 实现条件选股回测

基于 qstock 获取多只股票的历史价格数据,对选股结果进行定期调仓回测。

Equity Curve

导入包和定义获取、处理数据、绩效评价和绘图等函数

qstock由“Python 金融量化”公众号开发,可用于获取各个市场的实时和历史数据。本文基于 qstock 获取历史数据,对选股结果进行回测。

Python
# 导入包
import pandas as pd
import qstock as qs
from tqdm import tqdm
import warnings
from highcharts import Highstock


# 定义函数
# 获取调仓日以及持仓的股票
def get_change_position_date_and_stocks(selected_stocks):
    # 根据报告期对应的截至日期,提取出每季度选出的股票代码
    report_date_and_stocks = (
        selected_stocks.groupby("报告日期的截止日")["股票代码"]
        .apply(lambda g: g.values.tolist())
        .to_dict()
    )
    # 将报告报告期对应的截至日期转换为调仓日期
    change_position_date_list = []
    for report_date in report_date_and_stocks.keys():
        # 若报告期的截止日是第一季度的最后一天,则对应的调仓日是4月30日
        if report_date.month == 3:
            change_position_date = "{}{}{}".format(report_date.year, "04", "30")
        # 若报告期的截止日是第二季度的最后一天,则对应的调仓日是8月31日
        elif report_date.month == 6:
            change_position_date = "{}{}{}".format(report_date.year, "08", "31")
        # 若报告期的截止日是第三季度的最后一天,则对应的调仓日是10月31日
        elif report_date.month == 9:
            change_position_date = "{}{}{}".format(report_date.year, "10", "31")
        else:
            raise ValueError
        change_position_date_list.append(change_position_date)
    change_position_date_and_stocks = dict(
        zip(change_position_date_list, report_date_and_stocks.values())
    )
    return change_position_date_and_stocks


# 获取多只股票的每日收盘价格
def get_price_of_many_stocks(code_list, start, end):
    price = pd.DataFrame()
    for code in tqdm(code_list, leave=False):
        price_stock = qs.get_data(
            code_list=code, start=start, end=end, freq="d", fqt=2
        )["close"]
        price_stock.name = code
        price = pd.concat([price, price_stock], axis=1)
    return price


# 将收益率数据转换为价格数据
def prices_from_returns(returns):
    ret = 1 + returns
    ret.iloc[0] = 1
    return ret.cumprod()


# 计算策略评价指标
def strategy_evaluate(equity):
    """
    :param equity:  每天的资金曲线
    :return:
    """

    # ===新建一个dataframe保存回测指标
    results = pd.DataFrame()

    # 将数字转为百分数
    def num_to_pct(value):
        return "%.2f%%" % (value * 100)

    # ===计算累积净值
    results.loc[0, "累积净值"] = round(equity["组合净值"].iloc[-1], 2)

    # ===计算年化收益
    annual_return = (equity["组合净值"].iloc[-1]) ** (
        1 / (equity["交易日期"].iloc[-1] - equity["交易日期"].iloc[0]).days * 365
    ) - 1
    results.loc[0, "年化收益"] = str(round(annual_return * 100, 2)) + "%"

    # ===计算最大回撤,最大回撤的含义:《如何通过3行代码计算最大回撤》https://mp.weixin.qq.com/s/Dwt4lkKR_PEnWRprLlvPVw
    # 计算当日之前的资金曲线的最高点
    equity["max2here"] = equity["组合净值"].expanding().max()
    # 计算到历史最高值到当日的跌幅,drowdwon
    equity["dd2here"] = equity["组合净值"] / equity["max2here"] - 1
    # 计算最大回撤,以及最大回撤结束时间
    end_date, max_draw_down = tuple(
        equity.sort_values(by=["dd2here"]).iloc[0][["交易日期", "dd2here"]]
    )
    # 计算最大回撤开始时间
    start_date = (
        equity[equity["交易日期"] <= end_date]
        .sort_values(by="组合净值", ascending=False)
        .iloc[0]["交易日期"]
    )
    # 将无关的变量删除
    equity.drop(["max2here", "dd2here"], axis=1, inplace=True)
    results.loc[0, "最大回撤"] = format(max_draw_down, ".2%")
    results.loc[0, "最大回撤开始时间"] = str(start_date)
    results.loc[0, "最大回撤结束时间"] = str(end_date)

    # ===年化收益/回撤比:
    results.loc[0, "年化收益/回撤比"] = round(annual_return / abs(max_draw_down), 2)

    # ===每年、每月收益率
    equity.set_index("交易日期", inplace=True)
    year_return = (
        equity[["组合收益率", "指数收益率"]]
        .resample(rule="A")
        .apply(lambda x: (1 + x).prod() - 1)
    )
    monthly_return = (
        equity[["组合收益率", "指数收益率"]]
        .resample(rule="M")
        .apply(lambda x: (1 + x).prod() - 1)
    )

    year_return["超额收益"] = year_return["组合收益率"] - year_return["指数收益率"]
    monthly_return["超额收益"] = monthly_return["组合收益率"] - monthly_return["指数收益率"]

    year_return["组合收益率"] = year_return["组合收益率"].apply(num_to_pct)
    year_return["指数收益率"] = year_return["指数收益率"].apply(num_to_pct)
    year_return["超额收益"] = year_return["超额收益"].apply(num_to_pct)

    monthly_return["组合收益率"] = monthly_return["组合收益率"].apply(num_to_pct)
    monthly_return["指数收益率"] = monthly_return["指数收益率"].apply(num_to_pct)
    monthly_return["超额收益"] = monthly_return["超额收益"].apply(num_to_pct)

    return results.T, year_return, monthly_return


# 绘制净值曲线
def draw_equity_curve(
    prices, returns_data=False, title="Equity Curve", output_path=None
):
    # 将传入的数据修改为数据框
    if not isinstance(prices, pd.DataFrame):
        warnings.warn("prices are not in a dataframe", RuntimeWarning)
        prices = pd.DataFrame(prices)
    # 将索引修改为日期时间格式
    if not isinstance(prices.index, pd.DatetimeIndex):
        prices.index = pd.to_datetime(prices.index)
    # 如果传入的是收益率数据,则需要转换成价格数据
    if returns_data:
        prices = prices_from_returns(prices)
    # 初始化绘图对象
    H = Highstock()
    # 导入每一个资产的价格数据
    for column in prices.columns:
        H.add_data_set(
            data=prices[column].reset_index().values.tolist(),
            series_type="line",
            name=column,
        )
    # 设置绘图参数
    options = {
        "title": {"text": title},
        "rangeSelector": {"selected": 5},  # 1-5的数字代表默认观察窗口为1m、3m、6m、YTM、1y和All
        "yAxis": {
            "labels": {
                "formatter": "function () {\
                                return (this.value > 0 ? ' + ' : '') + this.value + '%';\
                            }"
            },  # this.value > 0 ? ' + '可以在正收益的数值前加上“+”
            # 绘制纵轴为0的横线
            "plotLines": [{"value": 0, "width": 2, "color": "silver"}],
        },
        "plotOptions": {"series": {"compare": "percent"}},
        "tooltip": {
            "pointFormat": '<span style="color:{series.color}">{series.name}:</span> <b>{point.y}</b> ({point.change}%)<br/>',
            "valueDecimals": 2,  # 默认显示的小数位
        },
    }
    # 应用绘图参数
    H.set_dict_options(options)
    # 如果指定了输出路径,则输出html文件到这个路径
    if output_path:
        f = open("{}.html".format(output_path), "w")
        f.write(H.htmlcontent)
        f.close()
    return H
Python
# 读取历史选股数据(每季度 15 只)
selected_stocks = pd.read_csv(
    "./selected_stocks.csv", parse_dates=["报告日期的截止日"], encoding="gbk"
)  # parse_dates 将'报告日期的截止日'这一列识别为日期格式,gbk 编码支持中文
# 截取“股票代码”和“报告日期的截止日”这两列
selected_stocks = selected_stocks[["股票代码", "报告日期的截止日"]]
# 将“股票代码”的后缀“.SH”去掉
selected_stocks["股票代码"] = selected_stocks["股票代码"].apply(lambda x: x[:-3])
# 获取调仓日以及持仓的股票
change_position_date_and_stocks = get_change_position_date_and_stocks(selected_stocks)
# 获取调仓日的列表
change_position_date_list = list(change_position_date_and_stocks.keys())

计算组合收益率(使用简单加权的粗略算法)

使用简单加权的粗略算法计算组合收益率,这一方法存在一定的缺陷。

例如,若资产组合包含两个资产,第一个资产每天的收益率都是\(-10\%\),第二个资产每天的收益率都是\(+10\%\)

按照简单加权的粗略算法,第一天,第一个资产的价格变成了\(1\times(1-10\%)=0.9\),第二个资产的价格变成了\(1\times(1+10\%)=1.1\),因此资产组合的净值是\(\frac{0.9+1.1}{2}=1\)。根据简单加权的粗略算法,资产组合在第一天的收益率是\(\frac{-10\%+10\%}{2}=0\)。这个结果在第一天确实是对的。

但是,第二天的时候,第一个资产的价格变成了\(0.9\times(1-10\%)=0.81\),第二个资产的价格变成了\(1.1\times(1+10\%)=1.21\),因此资产组合的净值是\(\frac{0.81+1.21}{2}=1.005\),因此资产组合在第二天的收益率是 0.5%。但是,根据简单加权的粗略算法,资产组合在第一天的收益率仍然是\(\frac{-10\%+10\%}{2}=0\)

造成这一差异的根本原因在于,资产组合内部的权重是会随着各资产的涨跌而改变的。一个资产上涨得越多,它的涨跌幅对整个资产组合涨跌幅的影响也越大。

这里为了简便,只使用简单加权的粗略算法计算组合收益率。更为精确的算法可以是:将各股票看作是投资组合的一部分,每天跟踪这一部分净值的变化,到下一调仓日再将各部分的净值相加,即得到整个投资组合的净值。

Python
# 创建空数据框,用于存放组合收益率
portfolio_return = pd.DataFrame()
# 对每一个调仓日,获取当日的股票价格,将价格转换为收益率,并计算组合在这段持有期的收益率
pbar = tqdm(change_position_date_list)
for change_position_date in pbar:
    pbar.set_description("正在计算从%s开始的组合每日收益率" % change_position_date)
    # 获取这一个调仓日至下一个调仓日之间的持仓股票价格
    code_list = change_position_date_and_stocks[change_position_date]
    # 获取下一个调仓日
    if (
        change_position_date == change_position_date_list[-1]
    ):  # 如果是最后一个调仓日,则下一个调仓日在change_position_date_list中找不到,需要手动设置为2022年8月31日
        next_change_position_date = "20220831"
    else:  # 否则下一个调仓日可以直接在change_position_date_list中找到
        next_change_position_date = change_position_date_list[
            change_position_date_list.index(change_position_date) + 1
        ]
    # 获取这一个调仓日至下一个调仓日之间的持仓股票价格
    stock_price = get_price_of_many_stocks(
        code_list=code_list, start=change_position_date, end=next_change_position_date
    )  # fqt=2表示后复权
    # 将价格转换为收益率,且删除全是空值的行(即第一行)
    stock_return = stock_price.pct_change().dropna(how="all")
    # 对收益率进行加权求和,得到组合收益率,并将结果添加到portfolio_return中
    portfolio_return_in_this_quarter = stock_return.mul(
        1 / len(stock_return.columns), axis=1
    ).sum(axis=1)
    portfolio_return = pd.concat(
        [portfolio_return, portfolio_return_in_this_quarter], axis=0
    )
正在计算从20220430开始的组合每日收益率: 100%|██████████| 37/37 [01:11<00:00, 1.94s/it]

获取组合净值和基准指数净值

Python
# 将组合收益率转换为累计净值
portfolio_net_value = prices_from_returns(portfolio_return)
# 修改列名
portfolio_net_value.columns = ["组合净值"]
# 修改索引名
portfolio_net_value.index.name = "date"
Python
# 获取中证500指数的价格
index_price = qs.get_price(
    code_list=["中证500"], start="20100504", end="20220831", freq="d", fqt=2
)
# 将中证500指数的价格归一化
index_price = index_price.div(index_price.iloc[0])
100%|██████████| 1/1 [00:00<00:00, 1004.38it/s]
Python
# 将组合收益率和中证 500 指数的价格合并
equity = portfolio_net_value.merge(
    index_price, how="left", left_index=True, right_index=True
)

绘制净值曲线

Python
# 绘制组合净值和中证500指数的净值曲线
draw_equity_curve(equity)

Equity Curve

Python
# 修改列名
equity.rename(columns={"中证 500": "指数收益率"}, inplace=True)
# 计算组合收益率
equity["组合收益率"] = equity["组合净值"].pct_change()
# 计算指数收益率
equity["指数收益率"] = equity["指数收益率"].pct_change()
# 生成交易日期列
equity["交易日期"] = equity.index

绩效评价

Python
# 计算策略评价指标
overall_performance, year_return, month_return = strategy_evaluate(equity)
Python
overall_performance
0
累积净值 6.31
年化收益 16.11%
最大回撤 -56.75%
最大回撤开始时间 2015-06-12 00:00:00
最大回撤结束时间 2018-10-18 00:00:00
年化收益/回撤比 0.28
Python
year_return
组合收益率 指数收益率 超额收益
交易日期
2010-12-31 -3.12% 11.33% -14.45%
2011-12-31 -26.29% -33.83% 7.53%
2012-12-31 32.59% 0.28% 32.31%
2013-12-31 36.37% 16.89% 19.48%
2014-12-31 78.17% 39.01% 39.17%
2015-12-31 68.05% 43.12% 24.93%
2016-12-31 -3.76% -17.78% 14.02%
2017-12-31 -6.39% -0.20% -6.19%
2018-12-31 -36.16% -33.32% -2.84%
2019-12-31 27.43% 26.38% 1.05%
2020-12-31 49.32% 20.87% 28.45%
2021-12-31 44.83% 15.58% 29.24%
2022-12-31 3.05% -16.36% 19.40%
Python
month_return
组合收益率 指数收益率 超额收益
交易日期
2010-05-31 -10.60% -7.44% -3.17%
2010-06-30 -8.02% -10.72% 2.70%
2010-07-31 15.30% 14.37% 0.93%
2010-08-31 10.09% 9.50% 0.59%
2010-09-30 -9.61% 1.55% -11.16%
... ... ... ...
2022-04-30 -10.71% -11.02% 0.31%
2022-05-31 4.69% 7.08% -2.38%
2022-06-30 5.59% 7.10% -1.51%
2022-07-31 6.65% -2.48% 9.13%
2022-08-31 -4.40% -2.20% -2.20%

148 rows × 3 columns

评论