量化基础算法:K线涨跌幅复权算法揭秘和实现

在量化过程中,我们经常用到K线的复权算法,有经典算法,递归前/后复权法,涨跌幅复权法等。由于前两个算法在收益率和涨跌幅计算方面存在不准确现象(至于怎么不准确,可以参考wind资讯复权算法说明文档),所以现在流行用涨跌幅复权法来计算,这里就主要阐述涨跌幅复权算法,其他的算法有兴趣可以查询相关文档。

涨跌幅复权算法

算法简单直接:在每次除权发生后, 根据除权价和前一收盘价计算一个比率,称为除权因子;把截止到计算日历次的除权因子连乘,即为截止日的累积除权因子。计算前复权价,则以价格乘上累积除权因子;向后复权,则价格除以累积除权因子。

根据涨跌幅复权法,首先要计算除权因子:

除权收盘价 = 除权除息价 = (股权登记日的收盘价-每股所分红利现金额+配股价×每股配股数)÷(1+每股送红股数+每股配股数+每股转增股数)

除权因子 = 除权收盘价 / 除权登记日收盘价

后复权上市首日后除权因子为1,前复权最近一次除权除息日后的交易日前复权因子为1。

除权会不断发生,所以需要计算出累积除权因子

累积除权因子 = 上一个除权因子 * (除权收盘价 / 除权登记日收盘价)

前复权因子 = 累积除权因子

后复权因子 = 1.0 / 累积除权因子

因为后复权价=价格 / 累积除权因子,所以后复权因子=1.0/累积除权因子,这样就能用后复权因子*价格来计算每个k线的后复权价格了

前复权价格 = 价格 * 前复权因子
后复权价格 = 价格 * 后复权因子

核心代码实现

完整代码已托管到 GitHub:https://github.com/dsxkline/dsx_base_algorithm

    def get_qfq(self):
        # 保存复权因子
        factors = {}
        # 前复权最近一次除权除息日除权因子为1
        right_factor = 1.0
        # 复权因子 前复权因子初始值为 1.0
        fq_factor = 1.0
        # 保存前一个分红配股信息
        back_share = None
        # 保存前一个对股价信息
        back_important = None
        # K线数据按日期倒叙排列
        klines = self.klines.copy()
        # 按日期倒序排列
        klines.reverse()
        for kline in klines:
            date,o,h,l,close,v,a = kline
            # 寻找除权登记日分红配股信息
            share = self.get_sharebonus(date)
            if back_share!=None:
                right_factor *= self.get_sharebonus_right_factor(share,close)
                # 前复权因子
                fq_factor = right_factor
                # 保存前复权因子
                factors[share["share_day"]] = fq_factor
                    
            # 处理早期股改阶段,一般是零几年的时候出现 对价送股派送现金的现象
            important = self.get_important(date)
            if back_important!=None:
                right_factor *= self.get_important_right_factor(important,close)
                # 前复权因子
                fq_factor = right_factor
                # 前复权因子
                factors[back_important['list_day']] = fq_factor

            # 保存当前配股和对股价信息
            back_share = share
            back_important = important
        return factors

    def get_hfq(self):
        # 保存复权因子
        factors = {}
        # 最近一次除权除息日除权因子为1
        right_factor = 1.0
        hfq_factor = 1.0
        last_close = 0
        klines = self.klines.copy()
        # 按日期顺序递归
        for kline in klines:
            date,o,h,l,close,v,a = kline
            # 寻找除权登记日分红配股信息
            share = self.get_sharebonus(date)
            if share!=None and last_close>0:
                right_factor *= self.get_sharebonus_right_factor(share,last_close)
                # right_factor = float(("%.7f"%right_factor))
                # 后复权因子
                hfq_factor = 1.0 / right_factor
                # hfq_factor = float(("%.6f"%hfq_factor))
                factors[share["share_day"]] = hfq_factor

            # 处理早期股改阶段,一般是零几年出现 对价送股派送现金的现象
            important = self.get_important(date)
            if important!=None:
                right_factor *= self.get_important_right_factor(important,last_close)
                # right_factor = float(("%.7f"%right_factor))
                # 后复权因子
                hfq_factor = 1.0 / right_factor
                # hfq_factor = float(("%.6f"%hfq_factor))
                factors[important['list_day']] = hfq_factor

            # 保存上一个交易日收盘价
            last_close = close
        return factors
    
    def get_sharebonus_right_factor(self,sharebonus:dict,close:float):
        """根据分红配股计算除权因子

        Args:
            sharebonus (dict): 分红配股数据
            close (float): 股权登记日的收盘价 
            right_factor (float): 上一个 除权因子
        Returns:
            float: 除权因子
        """
        if sharebonus!=None and close>0:
            per_ten_send = float(sharebonus['per_ten_send']) # 每10股送股
            per_ten_incr = float(sharebonus['per_ten_incr']) # 每10股转增
            per_cash_div = float(sharebonus['per_cash_div']) # 分红派息
            per_ten_allo = float(sharebonus['per_ten_allo']) # 每10股配股
            allo_price = float(sharebonus['allo_price']) # 配股价
            # 除权收盘价=除权除息价=(股权登记日的收盘价-每股所分红利现金额+配股价×每股配股数)÷(1+每股送红股数+每股配股数+每股转增股数);
            rm_right_close = (close - per_cash_div/10.0 + allo_price * (per_ten_allo/10.0) ) / (1.0+(per_ten_send/10.0) + (per_ten_allo/10.0) + (per_ten_incr/10.0) )
            # 除权因子= 除权收盘价 / 除权登记日收盘价
            right_factor = rm_right_close / close
            # right_factor = float(("%.4f"%right_factor))
            return right_factor
    
    def get_important_right_factor(self,important:dict,close:float):
        """根据对股价数据计算除权因子

        Args:
            important (dict): 对股价数据
            close (float): 股权登记日的收盘价
        Returns:
            float: 除权因子
        """
        
        if important!=None and close>0:
            per_ten_send = float(important['per_ten_send']) # 每10股送股
            per_ten_cash = float(important['per_ten_cash']) # 每10股派送现金
            # 除权收盘价=除权除息价=(股权登记日的收盘价-每股所分红利现金额+配股价×每股配股数)÷(1+每股送红股数+每股配股数+每股转增股数);
            rm_right_close = (close - per_ten_cash/10.0 ) / (1.0+(per_ten_send/10.0))
            # 除权因子= 除权收盘价 / 除权登记日收盘价
            right_factor = rm_right_close / close
            # right_factor = float(("%.6f"%right_factor))
            return right_factor

通过复权因子计算复权K线

    # 前复权
    factors = KlineFactors(klines,sharebonus,importants).get_qfq()
    # print(factors)

    # 根据复权因子计算复权K线
    qfq_kline = []
    for item in klines:
        date,o,h,l,c,v,a = item
        # 查找日期之后的复权因子,如果查不到即为首次复权为1.0
        factor = 1.0
        fd = ""
        for d in list(factors.keys()):
            if int(date)<int(d.replace("-","")):
                factor = factors.get(d)
                fd = d
        # 计算复权数据
        o *= factor
        h *= factor
        l *= factor
        c *= factor
        a = round(a/v * factor * v,2)
        newitem = [date,round(o,2),round(h,2),round(l,2),round(c,2),v,a,fd,factor]
        if str(date).startswith("1999"):
            print(newitem)
        qfq_kline.append(newitem)
    # print(qfq_kline)    

    # 后复权
    factors = KlineFactors(klines,sharebonus,importants).get_hfq()
    # print(factors)

    # 根据后权因子计算复权K线
    hfq_kline = []
    for item in klines:
        date,o,h,l,c,v,a = item
        # 查找日期之前的复权因子,如果查不到即为首次复权为1.0
        factor = 1.0
        fd = ""
        for d in list(factors.keys()):
            if int(date)>=int(d.replace("-","")):
                factor = factors.get(d)
                fd = d
        # 计算复权数据
        o *= factor
        h *= factor
        l *= factor
        c *= factor
        newitem = [date,round(o,2),round(h,2),round(l,2),round(c,2),v,a,fd,factor]
        if str(date).startswith("2023"):
            print(newitem)
        hfq_kline.append(newitem)
    # print(hfq_kline)

打印输出

前复权打印:
['19991110', 1.98, 2.0, 1.82, 1.87, 1740850.0, 326657616.77, '2000-07-06', 0.06722592297375657]
['19991111', 1.85, 1.91, 1.85, 1.86, 294034.0, 55231608.25, '2000-07-06', 0.06722592297375657]
['19991112', 1.87, 1.9, 1.87, 1.89, 150079.0, 28341844.09, '2000-07-06', 0.06722592297375657]
['19991115', 1.9, 1.9, 1.86, 1.87, 119210.0, 22383005.51, '2000-07-06', 0.06722592297375657]
['19991116', 1.87, 1.88, 1.78, 1.78, 232231.0, 42278920.77, '2000-07-06', 0.06722592297375657]
['19991117', 1.78, 1.83, 1.77, 1.83, 100525.0, 18083437.15, '2000-07-06', 0.06722592297375657]
['19991118', 1.83, 1.85, 1.8, 1.82, 84465.0, 15433525.72, '2000-07-06', 0.06722592297375657]
['19991119', 1.85, 1.85, 1.8, 1.81, 53749.0, 9807388.22, '2000-07-06', 0.06722592297375657]
['19991122', 1.81, 1.81, 1.77, 1.78, 55354.0, 9887992.11, '2000-07-06', 0.06722592297375657]
['19991123', 1.78, 1.78, 1.75, 1.78, 38439.0, 6804876.83, '2000-07-06', 0.06722592297375657]
['19991124', 1.78, 1.78, 1.75, 1.78, 40980.0, 7216299.48, '2000-07-06', 0.06722592297375657]
['19991125', 1.77, 1.79, 1.75, 1.77, 57252.0, 10119383.73, '2000-07-06', 0.06722592297375657]
['19991126', 1.78, 1.79, 1.76, 1.78, 22826.0, 4067706.15, '2000-07-06', 0.06722592297375657]
['19991129', 1.78, 1.8, 1.76, 1.77, 26812.0, 4779695.9, '2000-07-06', 0.06722592297375657]
['19991130', 1.77, 1.78, 1.76, 1.77, 23713.0, 4190527.91, '2000-07-06', 0.06722592297375657]
['19991201', 1.77, 1.81, 1.76, 1.79, 28651.0, 5128463.99, '2000-07-06', 0.06722592297375657]
['19991202', 1.78, 1.8, 1.76, 1.77, 19384.0, 3435916.92, '2000-07-06', 0.06722592297375657]
['19991203', 1.76, 1.79, 1.76, 1.77, 25525.0, 4523699.58, '2000-07-06', 0.06722592297375657]
['19991206', 1.77, 1.77, 1.72, 1.73, 69839.0, 12135354.71, '2000-07-06', 0.06722592297375657]
['19991207', 1.72, 1.74, 1.71, 1.72, 39557.0, 6821212.73, '2000-07-06', 0.06722592297375657]
['19991208', 1.72, 1.73, 1.71, 1.72, 22365.0, 3846263.96, '2000-07-06', 0.06722592297375657]
['19991209', 1.71, 1.72, 1.7, 1.7, 25646.0, 4378827.72, '2000-07-06', 0.06722592297375657]
['19991210', 1.7, 1.75, 1.7, 1.75, 35539.0, 6120315.25, '2000-07-06', 0.06722592297375657]
['19991213', 1.75, 1.78, 1.72, 1.74, 70584.0, 12413602.81, '2000-07-06', 0.06722592297375657]
['19991214', 1.73, 1.75, 1.73, 1.75, 16184.0, 2813068.75, '2000-07-06', 0.06722592297375657]
['19991215', 1.75, 1.81, 1.74, 1.78, 67978.0, 12128027.09, '2000-07-06', 0.06722592297375657]
['19991216', 1.78, 1.78, 1.74, 1.75, 36162.0, 6359639.54, '2000-07-06', 0.06722592297375657]
['19991217', 1.75, 1.76, 1.71, 1.72, 35653.0, 6171810.31, '2000-07-06', 0.06722592297375657]
['19991221', 1.71, 1.72, 1.69, 1.7, 42153.0, 7161106.99, '2000-07-06', 0.06722592297375657]
['19991222', 1.7, 1.7, 1.68, 1.69, 30835.0, 5217672.79, '2000-07-06', 0.06722592297375657]
['19991223', 1.69, 1.7, 1.66, 1.67, 41615.0, 6961714.91, '2000-07-06', 0.06722592297375657]
['19991224', 1.65, 1.69, 1.65, 1.66, 28324.0, 4715360.69, '2000-07-06', 0.06722592297375657]
['19991227', 1.66, 1.68, 1.65, 1.65, 22337.0, 3708854.17, '2000-07-06', 0.06722592297375657]
['19991228', 1.65, 1.69, 1.65, 1.65, 31766.0, 5272529.14, '2000-07-06', 0.06722592297375657]
['19991229', 1.66, 1.68, 1.65, 1.66, 22842.0, 3793962.19, '2000-07-06', 0.06722592297375657]
['19991230', 1.67, 1.68, 1.66, 1.66, 23331.0, 3891574.23, '2000-07-06', 0.06722592297375657]

后复权打印:
['20230103', 108.14, 108.29, 106.66, 107.55, 258925.0, 187094064.0, '2022-07-21', 14.875214140092607]
['20230104', 108.14, 109.33, 107.55, 108.74, 309471.0, 226321372.0, '2022-07-21', 14.875214140092607]
['20230105', 109.63, 109.78, 108.59, 109.33, 301622.0, 221617355.0, '2022-07-21', 14.875214140092607]
['20230106', 109.33, 109.78, 108.74, 109.18, 203129.0, 149170538.0, '2022-07-21', 14.875214140092607]
['20230109', 109.78, 109.78, 108.59, 109.18, 196123.0, 143998211.0, '2022-07-21', 14.875214140092607]
['20230110', 109.33, 109.33, 108.29, 108.29, 161946.0, 118223822.0, '2022-07-21', 14.875214140092607]
['20230111', 108.74, 109.18, 108.14, 108.59, 192155.0, 140393996.0, '2022-07-21', 14.875214140092607]
['20230112', 109.04, 109.18, 107.7, 108.14, 168499.0, 122554362.0, '2022-07-21', 14.875214140092607]
['20230113', 108.44, 109.33, 108.14, 109.18, 217702.0, 159178132.0, '2022-07-21', 14.875214140092607]
['20230116', 109.33, 109.78, 108.29, 109.04, 368169.0, 270170018.0, '2022-07-21', 14.875214140092607]
['20230117', 109.18, 109.33, 108.14, 108.59, 242226.0, 176749046.0, '2022-07-21', 14.875214140092607]
['20230118', 108.74, 109.04, 108.14, 108.74, 244946.0, 178868885.0, '2022-07-21', 14.875214140092607]
['20230119', 108.74, 109.04, 107.99, 109.04, 167134.0, 121983726.0, '2022-07-21', 14.875214140092607]
['20230120', 109.18, 109.48, 108.89, 109.48, 209870.0, 154239831.0, '2022-07-21', 14.875214140092607]
['20230130', 110.52, 110.52, 109.63, 110.08, 318107.0, 235352030.0, '2022-07-21', 14.875214140092607]
['20230131', 110.37, 110.82, 109.63, 109.63, 284382.0, 210715444.0, '2022-07-21', 14.875214140092607]
['20230201', 110.08, 110.08, 109.04, 109.48, 186595.0, 137294549.0, '2022-07-21', 14.875214140092607]
['20230202', 109.93, 109.93, 108.89, 109.48, 219002.0, 160961795.0, '2022-07-21', 14.875214140092607]
['20230203', 109.04, 109.33, 108.14, 108.14, 298855.0, 217904608.0, '2022-07-21', 14.875214140092607]

(计算结果前复权跟wind和雪球一致,后复权相差0.08个点)有没有人知道这是为什么?

代码完整开源可运行,感兴趣可去GitHub下载,有问题可私信,欢迎交流学习。

ChatGPT 对于涨跌幅的理解:

在股票市场中,涨跌幅复权是一种常见的算法,用于调整股票价格以反映分红和拆股等公司行动对股价的影响。

涨跌幅复权算法的基本思想是,在股票价格中调整股票的历史涨跌幅,以反映公司行动对股价的影响。具体来说,假设股票在某个时间点上涨了一定比例的价格,那么在调整后的股价中,这个价格涨幅所占比例应该与公司行动前的股价相同。

下面是涨跌幅复权算法的一个示例:

假设一只股票在某一天的收盘价为 $100。在第二天,该公司宣布拆股,比例为 1:2。在这种情况下,我们需要将前一天的股价乘以 1/2,以反映股票数量增加的影响。

现在,假设第三天的收盘价为 $120。如果我们只考虑股票的价格变化,那么涨幅为 20%。但是,在进行涨跌幅复权后,我们会发现,第二天的调整后的股价为 $50(即前一天的收盘价 $100 乘以 1/2),因此,实际上第三天的涨幅是 140%(即 $120/$50 – 1)。

需要注意的是,涨跌幅复权算法是一种相对复杂的算法,实际应用中需要考虑许多因素,如股票分红和拆股的比例、税收和交易费用等。因此,建议在实际应用中使用专业的金融软件或咨询专业人士。