基于高股息高分红优化的量化选股模型

注:以下内容不构成任何投资建议,仅作为数据分析学习用途公开
该模型是中金公司提出的,自然语言描述如下:

构造基于高分红高股息优化的量化选股思路。我们基于前述分析,构造基于分红、股息率、自由现金流等指标的选股模型,具体要求:1)筛选时间点:每年4月底年报发布完毕后,统一按年度频率筛选。2)市值、市盈率要求:市值大于50亿元,市盈率为正但小于25倍。3)股息率和分红标准:非金融公司股息率大于3%,分红比例当年大于45%或者3年平均大于45%;金融股息率大于5%,当年分红比例大于35%或3年平均大于35%。4)自由现金流标准:金融无现金流要求;非金融自由现金流/所有者权益大于8%;5)其它:3年平均ROE金融大于10%,非金融大于8%。

全文见这里
基于中金公司的模型描述,以及Baostock金融数据接口和某花顺金融数据,使用Python3.9实现,直接上代码:

import time
import baostock as bs
import pandas as pd
import requests
import json
import datetime
from bs4 import BeautifulSoup
import pickle
# 获取两市股票列表,注:sz代表深市,sh代表沪市
pd.set_option('display.max_columns', None)
myruilicence = 'bfbfudsfuoisuiblblkj7b'
year = 2023 # 计算的年份
# 每日更新股票列表并写入文件,覆盖上次内容
get_stock_list_api = 'https://api.mairui.club/hslt/list/'+myruilicence
# resopnse = requests.get(get_stock_list_api)
# f = open('stocks.list', 'w')
# f.write(resopnse.text)
# f.close()
f = open('stocks2.list', 'r')
json_data = f.readline()
# 读取list格式的两市所有上市公司股票代码
stock_lists = json.loads(json_data)
f.close()
#### 基于高分红高股息优化的量化选股模型 ####
# 计算滚动市盈率peTTM
def getPeTTM(stock_code,start_date,end_date):
    rs = bs.query_history_k_data_plus(stock_code,
                                      "date,code,close,peTTM,pbMRQ,psTTM,pcfNcfTTM",
                                      start_date=start_date, end_date=end_date,
                                      frequency="d", adjustflag="3")
    result_list = []
    while (rs.error_code == '0') & rs.next():
        # 获取一条记录,将记录合并在一起
        result_list.append(rs.get_row_data())
    result = pd.DataFrame(result_list, columns=rs.fields)
    peTTM = float(result['peTTM'][0])
    return peTTM
# 计算上市公司已上市时间(天为单位,截至日期为程序运行时的当天)
def getIPODate(stock_code):
    rs = bs.query_stock_basic(code=stock_code)
    data_list = []
    while (rs.error_code == '0') & rs.next():
        # 获取一条记录,将记录合并在一起
        data_list.append(rs.get_row_data())
    result = pd.DataFrame(data_list, columns=rs.fields)
    ipoDate = datetime.datetime.strptime(result['ipoDate'][0], "%Y-%m-%d").date()
    nowDate = datetime.datetime.now().date()
    ipoDays = (nowDate-ipoDate).days
    return ipoDays
# 计算最新总市值
def getMK(stock_code,start_date,end_date,year):
    profit_list = []
    rs_profit = bs.query_profit_data(code=stock_code, year=year, quarter=1)
    while (rs_profit.error_code == '0') & rs_profit.next():
        profit_list.append(rs_profit.get_row_data())
    result_profit = pd.DataFrame(profit_list, columns=rs_profit.fields)
    totalShare = result_profit['totalShare']  # 提取总股本
    rs = bs.query_history_k_data_plus(stock_code,
                                      "date,code,open,high,low,close,preclose,volume,amount,adjustflag,turn,tradestatus,pctChg,isST",
                                      start_date=start_date, end_date=end_date,
                                      frequency="d", adjustflag="3")
    data_list = []
    while (rs.error_code == '0') & rs.next():
        data_list.append(rs.get_row_data())
    result = pd.DataFrame(data_list, columns=rs.fields)
    close_price = result['close']  # 提取最新收盘股价
    if result.empty or result_profit.empty:
        return [0,0,0]
    # 计算总市值
    totalMK = 0
    try:
        totalMK = float(close_price[0]) * float(totalShare[0])
    except KeyError as e:
        print("股票代码", stock_code, "市值计算错误")
    return [totalMK, close_price[0], totalShare[0]]
# 计算近三年平均股息率,以百分数点数形势返回,返回8.9表明股息率为8.9%
def getYield(stock_code,price,years):
    with open("stock_bonuses.pkl", "rb") as file:
        dict = pickle.load(file)
    file.close()
    # 提取最近三年的分红数据
    data = []
    try:
        for year in years:
            for item in dict[stock_code]:
                if str(item[0]).find(str(year)) != -1:
                    data.append(item)
    except KeyError as e:
        return 0
    Yield = 0
    for item in data:
        zeroindex = str(item[1]).find("不分配不转增")
        zgindex = str(item[1]).find("10转")
        sp1 = str(item[1]).find("10送")
        sp2 = str(item[1]).find("股派")
        # 计算派股的情形,派股暂时按0%计算股息
        if zeroindex == -1:
            try:
                bonous = 0
                if zgindex == -1:
                    if sp1 == -1 and sp2 == -1:
                        bonous = float(str(item[1]).replace("10派", "").replace("元(含税)", ""))
                    # 计算又送股又派股的情形
                    if sp1 != -1 and sp2 != -1:
                        bonous = float(str(item[1]).split("股派")[1].replace("元(含税)", ""))
                Yield  = Yield + bonous
            except ValueError as e:
                print("分红比例计算错误:", stock_code)
                print("错误结果", ((Yield/3)/float(price))*10)
                print(e)
    return ((Yield/3)/float(price))*10

# 提取同花顺的上市公司分红数据
def getBonus(stock_list):
    url_pre = "https://basic.10jqka.com.cn/mobile/"
    url_last = "/bonusn.html"
    headers = {
        'cookie':'v=g6',
        'sec-ch-ua': '"Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"',
        'sec-ch-ua-mobile':'?0',
        'sec-ch-ua-platform':'windows',
        'Upgrade-Insecure-Requests':'1',
        'User-Agent': 'Mozilla / 5.0(Windows NT 10.0; Win64; x64) AppleWebKit / 537.36(KHTML, like Gecko) Chrome / 116.0.0.0 Safari / 537.36',
        'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Sec-Fetch-Site': 'none',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-User': '?1',
        'Sec-Fetch-Dest': 'document',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'en, en - IN;q = 0.9'
    }

    dict = {}
    for stock_code in stock_list:
        stock_bonuses = []
        url = url_pre+stock_code+url_last
        # url = 'https://httpbin.org/headers'
        response = requests.get(url=url, headers=headers)
        time.sleep(0.5)
        response.encoding = 'utf8'
        html = response.content
        soup = BeautifulSoup(html, 'html.parser')
        years = soup.select(".trHead")  #财报年度和类型(年报/中报)
        tl = soup.select(".tl")   #分红情况
        for i in range(1, len(years)):
            key = str(years[i]).replace('<td class="trHead">', "").replace('<td class="tl">', '').replace("</td>", "")
            value = str(tl[i]).replace("</td>", "").replace('<td class="tl">', '')
            stock_bonuses.append([key, value])
        dict[stock_code] = stock_bonuses
    with open("stock_bonuses1.pkl", "wb") as file:
        pickle.dump(dict, file)
    file.close()

# 提取上市公司每年扣非净利润
def getDeductionOfNonNetProfit(stock_list):
    url_pre = "https://basic.10jqka.com.cn/basicapi/finance/stock/index/stack/?code="
    url_last = "&market=33&id=index_deduct_holder_net_profit&period=-1&locale=zh_CN"
    headers = {
        'cookie': 'v=f3',
        'sec-ch-ua': '"Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': 'windows',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla / 5.0(Windows NT 10.0; Win64; x64) AppleWebKit / 537.36(KHTML, like Gecko) Chrome / 116.0.0.0 Safari / 537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Sec-Fetch-Site': 'none',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-User': '?1',
        'Sec-Fetch-Dest': 'document',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'en, en - IN;q = 0.9'
    }

    dict = {}
    for stock_code in stock_list:
        DeductionOfNonNetProfit = []
        url = url_pre + stock_code + url_last
        response = requests.get(url=url, headers=headers)
        # time.sleep(0.5)
        response.encoding = 'utf8'
        json_data = response.text
        stock_lists = json.loads(json_data)
        for item in stock_lists["data"]["data"]:
            DeductionOfNonNetProfit.append(item)
        dict[stock_code] = DeductionOfNonNetProfit

    with open("DeductionOfNonNetProfit1.pkl", "wb") as file:
        pickle.dump(dict, file)
    file.close()
# 提取上市公司ROE数据
def getROE(stock_list):
    url_pre = "https://basic.10jqka.com.cn/basicapi/finance/stock/index/stack/?code="
    url_last = "&market=33&id=index_weighted_avg_roe&period=-1&locale=zh_CN"
    headers = {
        'cookie': 'v=d7',
        'sec-ch-ua': '"Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': 'windows',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla / 5.0(Windows NT 10.0; Win64; x64) AppleWebKit / 537.36(KHTML, like Gecko) Chrome / 116.0.0.0 Safari / 537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Sec-Fetch-Site': 'none',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-User': '?1',
        'Sec-Fetch-Dest': 'document',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'en, en - IN;q = 0.9'
    }
    dict = {}
    for stock_code in stock_list:
        ROE = []
        url = url_pre + stock_code + url_last
        response = requests.get(url=url, headers=headers)
        # time.sleep(0.5)
        response.encoding = 'utf8'
        json_data = response.text
        stock_lists = json.loads(json_data)
        for item in stock_lists["data"]["data"]:
            ROE.append(item)
        dict[stock_code] = ROE
    print(dict)
    with open("ROE1.pkl", "wb") as file:
        pickle.dump(dict, file)
    file.close()
# 提取上市公司所属行业
def queryIndustry(stock_code):
    rs = bs.query_stock_industry(code=stock_code)
    industry_list = []
    while (rs.error_code == '0') & rs.next():
        industry_list.append(rs.get_row_data())
    result = pd.DataFrame(industry_list, columns=rs.fields)
    return(result.industry[0])

# 计算一只股票指定年份的分红比例=(总股本*年报每股派息)/年报扣非净利润
def getDividendRatio(stock_code, totalShare, year):
    # 读取年报每股派息表
    with open("stock_bonuses.pkl", "rb") as file:
        bonusesData = pickle.load(file)
    file.close()
    # 读取净利润表
    with open("DeductionOfNonNetProfit.pkl", "rb") as file:
        deductionOfNonNetProfitData = pickle.load(file)
    bonuseindex = []
    for i in range(0, len(bonusesData[stock_code])):
        if str(bonusesData[stock_code][i][0]).find(str(year)) != -1 :
            bonuseindex.append(i)
    file.close()
    bonous = 0
    # 计算年度每股派息
    for i in bonuseindex:
        if bonusesData[stock_code][i][1].find("不分配不转增") == -1:
            zgindex = bonusesData[stock_code][i][1].find("10转")
            sp1 = str(bonusesData[stock_code][i]).find("10送")
            sp2 = str(bonusesData[stock_code][i]).find("股派")
            if zgindex == -1:
                if sp1 == -1 and sp2 == -1:
                    bonous = bonous + float(str(bonusesData[stock_code][i][1]).replace("10派", "").replace("元(含税)", ""))
                # 计算又送股又派股的情形
                if sp1 != -1 and sp2 != -1:
                    bonous = bonous + float(str(bonusesData[stock_code][i][1]).split("股派")[1].replace("元(含税)", ""))
            else:
                bonous = bonous + 0
    # 计算年度总分红金额
    totalBonous = (bonous/10)*totalShare
    # 提取年度扣非净利润
    deductionOfNonNetProfit = 0
    for item in deductionOfNonNetProfitData[stock_code]:
        if item["name"] == str(year)+"年报":
            deductionOfNonNetProfit = item["value"]
    # 计算指定年度的分红比例
    DividendRatio = totalBonous/float(deductionOfNonNetProfit)
    return DividendRatio*100

# 计算一只股票指定年份的ROE
def getROEbyYear(stock_code, year):
    # 读取ROE表
    with open("ROE.pkl", "rb") as file:
        ROEData = pickle.load(file)
    file.close()
    ROE = 0
    for item in ROEData[stock_code]:
        if item["name"] == str(year) + "年报":
            ROE = item["value"]
    return float(ROE)
# 提取上市公司每股经营现金流数据
def getOperatingCashFlow(stock_list):
    url_pre = "https://basic.10jqka.com.cn/basicapi/finance/stock/index/stack/?code="
    url_last = "&market=33&id=index_per_operating_cash_flow_net&period=-1&locale=zh_CN"
    headers = {
        'cookie': 'v=A123',
        'sec-ch-ua': '"Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': 'windows',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla / 5.0(Windows NT 10.0; Win64; x64) AppleWebKit / 537.36(KHTML, like Gecko) Chrome / 116.0.0.0 Safari / 537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Sec-Fetch-Site': 'none',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-User': '?1',
        'Sec-Fetch-Dest': 'document',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'en, en - IN;q = 0.9'
    }
    dict = {}
    for stock_code in stock_list:
        OperatingCashFlow = []
        url = url_pre + stock_code + url_last
        response = requests.get(url=url, headers=headers)
        # time.sleep(0.5)
        response.encoding = 'utf8'
        json_data = response.text
        stock_lists = json.loads(json_data)
        for item in stock_lists["data"]["data"]:
            OperatingCashFlow.append(item)
        dict[stock_code] = OperatingCashFlow
    print(dict)
    with open("OperatingCashFlow1.pkl", "wb") as file:
        pickle.dump(dict, file)
    file.close()

#判断一只股票是否最近三年经营现金流持续为正
def checkOperatingCashFlow(stock_code,year_list):
    # 读取每股经营现金流表
    with open("OperatingCashFlow.pkl", "rb") as file:
        OperatingCashFlowData = pickle.load(file)
    file.close()
    index = []
    for year in year_list:
        for i in range(0, len(OperatingCashFlowData[stock_code])):
            if OperatingCashFlowData[stock_code][i]["name"] == str(year) + "年报":
                index.append(i)
                break
    for i in index:
        value = float(OperatingCashFlowData[stock_code][i]["value"])
        if value < 0:
            return False
        else:
            pass
    return True


if __name__ == '__main__':
    # 登陆baostock API
    lg = bs.login()
    # 显示登陆返回信息
    if lg.error_code != '0':
        print('login respond error_code:' + lg.error_code)
        print('login respond  error_msg:' + lg.error_msg)
    else:
        for stock in stock_lists:
            code_pre = stock['dm']  # 读取股票代码
            exchange = stock['jys']  # 读取交易所
            stock_name = stock["mc"]      #读取股票名称
            stock_code = exchange + '.' + code_pre  # 合并成baostock API要求的股票代码
            date = '2023-11-07'
            rs = bs.query_stock_basic(code=stock_code)
            data_list = []
            while (rs.error_code == '0') & rs.next():
                # 获取一条记录,将记录合并在一起
                data_list.append(rs.get_row_data())
            result = pd.DataFrame(data_list, columns=rs.fields)
            try:
                status = str(result['status'][0])
                type = str(result['type'][0])
            except KeyError as e:
                status = -1
                type = -1
            # 剔除退市股以及非股票类权益
            if status == '1' and type == '1':
                totalMK = getMK(stock_code=stock_code, start_date=date, end_date=date, year=year)
                # 读取最新股价
                price = float(totalMK[1])
                # 读取总股本
                share = float(totalMK[2])
                # 条件一:筛掉市值小于50亿的股票
                if totalMK[0] > 5000000000:
                    # 条件二:筛选出滚动市盈率为正且小于25倍的公司
                    peTTM = getPeTTM(stock_code=stock_code, start_date=date, end_date=date)
                    if peTTM > 0 and peTTM < 25:
                        # 条件三:筛选出上市日期短于三年的股票
                        ipoDays = getIPODate(stock_code=stock_code)
                        if ipoDays > 365 * 3:
                            # 计算股息率
                            bonous = getYield(stock_code=code_pre, price=price, years=[2020, 2021, 2022])
                            # 判断行业
                            industry = queryIndustry(stock_code=stock_code)
                            # 金融股:股息率大于5%,当年分红比例大于35%或3年平均大于35%
                            if industry == "非银金融" or industry == "银行":
                                if bonous >= 5:
                                    # 计算当年分红比例和三年平均分红比例
                                    DividendRatio = getDividendRatio(stock_code=code_pre, totalShare=share, year=2022)
                                    if DividendRatio >= 0:
                                        # 计算年度ROE
                                        ROE = getROEbyYear(stock_code=code_pre, year=2022)
                                        if ROE > 10:
                                            print(stock_code, stock_name, industry, price, str(float(totalMK[0]) / 100000000) + "亿", peTTM,
                                                  str(ipoDays / 365) + "年", str(bonous) + "%", str(DividendRatio) + "%",
                                                  str(ROE))
                            else:
                                # 非金融股:股息率大于3%,分红比例当年大于45%或者3年平均大于45%
                                if bonous >= 3:
                                    # 计算当年分红比例和三年平均分红比例
                                    DividendRatio = getDividendRatio(stock_code=code_pre, totalShare=share, year=2022)
                                    if DividendRatio >= 45:
                                        # 计算三年平均ROE
                                        ROE = getROEbyYear(stock_code=stock_code.split(".")[1], year=2022)
                                        if ROE > 8:
                                            # 计算近三年经营现金流
                                            result = checkOperatingCashFlow(stock_code=code_pre,
                                                                            year_list=[2020, 2021, 2022])
                                            if result == True:
                                                print(stock_code, stock_name, industry,price, str(float(totalMK[0]) / 100000000) + "亿",
                                                      peTTM,
                                                      str(ipoDays / 365) + "年", str(bonous) + "%",
                                                      str(DividendRatio) + "%",
                                                      str(ROE))

    #### 登出系统 ####
    bs.logout()


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值