注:以下内容不构成任何投资建议,仅作为数据分析学习用途公开
该模型是中金公司提出的,自然语言描述如下:
构造基于高分红高股息优化的量化选股思路。我们基于前述分析,构造基于分红、股息率、自由现金流等指标的选股模型,具体要求: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()