diff --git a/vnpy/data/stock/README.md b/vnpy/data/stock/README.md new file mode 100644 index 00000000..8a479d31 --- /dev/null +++ b/vnpy/data/stock/README.md @@ -0,0 +1,31 @@ +股票类相关数据接口 + + +基础数据 + + + 获取/更新股票的基本资料 + 参考/下载: http://baostock.com/baostock/index.php/%E8%AF%81%E5%88%B8%E5%9F%BA%E6%9C%AC%E8%B5%84%E6%96%99 + python stock_base.py + 保存二进制对象dict{vt_symbol: dict} => stock_base.pkb2 + + +除权除息 + + 更新股票除权除息信息 (2006年) + 参考/下载: http://baostock.com/baostock/index.php/%E9%99%A4%E6%9D%83%E9%99%A4%E6%81%AF%E4%BF%A1%E6%81%AF + python stock_dividend.py + 保存csv文件=> stock_dividend.csv + +复权因子 + + 获取/更新股票复权因子(2006年开始) + 参考/下载: http://baostock.com/baostock/index.php/%E5%A4%8D%E6%9D%83%E5%9B%A0%E5%AD%90%E4%BF%A1%E6%81%AF + python adjust_factor.py + 保存二进制对象 dict {vt_symbol, []} => stock_adjust_factor.pkb2 + + +5分钟K线数据 + + + diff --git a/vnpy/data/stock/adjust_factor.py b/vnpy/data/stock/adjust_factor.py new file mode 100644 index 00000000..fd98a526 --- /dev/null +++ b/vnpy/data/stock/adjust_factor.py @@ -0,0 +1,123 @@ +# flake8: noqa +""" +# 追加/更新股票复权因子 +""" + +import os +import sys +import json +from typing import Any +from collections import OrderedDict +import pandas as pd + +vnpy_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) +if vnpy_root not in sys.path: + sys.path.append(vnpy_root) + +os.environ["VNPY_TESTING"] = "1" +import baostock as bs +from vnpy.trader.constant import Exchange +from vnpy.trader.utility import load_json, load_data_from_pkb2, save_data_to_pkb2, extract_vt_symbol +from vnpy.data.tdx.tdx_common import get_stock_type +from vnpy.data.stock.stock_base import get_stock_base +import baostock as bs +import pandas as pd + +ADJUST_FACTOR_FILE = 'stock_adjust_factor.pkb2' + + +def get_all_adjust_factor(): + """ 获取所有股票复权因子""" + cache_file_name = os.path.abspath(os.path.join(os.path.dirname(__file__), ADJUST_FACTOR_FILE)) + + data = load_data_from_pkb2(cache_file_name) + if data is None: + return download_adjust_factor() + else: + return data + +def get_adjust_factor(vt_symbol: str, stock_name: str = '', need_login: bool = True): + """ + 通过baostock,获取复权因子 + :param vt_symbol: + :param stock_name: + :param need_login: + :return: + """ + if need_login: + login_msg = bs.login() + if login_msg.error_code != '0': + print(f'证券宝登录错误代码:{login_msg.error_code}, 错误信息:{login_msg.error_msg}') + return [] + + symbol, exchange = extract_vt_symbol(vt_symbol) + bs_code = '.'.join(['sh' if exchange == Exchange.SSE else 'sz', symbol]) + + print(f'开始获取{stock_name} {bs_code}得复权因子') + rs = bs.query_adjust_factor( + code=bs_code, + start_date='2006-01-01' + ) + if rs.error_code != '0': + print(f'证券宝获取沪深A股复权因子数据,错误代码:{rs.error_code}, 错误信息:{rs.error_msg}') + return [] + + # [dict] => dataframe + + print(f'返回字段:{rs.fields}') + result_list = [] + while (rs.error_code == '0') and rs.next(): + row = rs.get_row_data() + exchange_code, stock_code = row[0].split('.') + d = { + 'exchange': exchange.value, # 证券交易所 + 'code': stock_code, # 证券代码 + 'name': stock_name, # 证券中文名称 + 'dividOperateDate': row[1], # 除权除息日期 + 'foreAdjustFactor': float(row[2]), # 向前复权因子 除权除息日前一个交易日的收盘价/除权除息日最近的一个交易日的前收盘价 + 'backAdjustFactor': float(row[3]), # 向后复权因子 除权除息日最近的一个交易日的前收盘价/除权除息日前一个交易日的收盘价 + 'adjustFactor': float(row[4]) # 本次复权因子 + + } + result_list.append(d) + + print(f'{d}') + return result_list + + +def download_adjust_factor(): + """ + 下载更新股票复权因子 + :return: + """ + + # 获取所有股票基础信息 + base_dict = get_stock_base() + + # 尝试从本地缓存获取 + cache_file_name = os.path.abspath(os.path.join(os.path.dirname(__file__), ADJUST_FACTOR_FILE)) + factor_dict = load_data_from_pkb2(cache_file_name) + if factor_dict is None: + factor_dict = dict() + + login_msg = bs.login() + if login_msg.error_code != '0': + print(f'证券宝登录错误代码:{login_msg.error_code}, 错误信息:{login_msg.error_msg}') + return + + for k, v in base_dict.items(): + if v.get('类型') != '股票': + continue + + factor_list = get_adjust_factor(vt_symbol=k, stock_name=v.get('name'), need_login=False) + + if len(factor_list) > 0: + factor_dict.update({k: factor_list}) + + if len(factor_dict) > 0: + save_data_to_pkb2(factor_dict, cache_file_name) + print(f'保存除权除息至文件:{cache_file_name}') + + +if __name__ == '__main__': + download_adjust_factor() diff --git a/vnpy/data/stock/stock_base.py b/vnpy/data/stock/stock_base.py new file mode 100644 index 00000000..849ecb3d --- /dev/null +++ b/vnpy/data/stock/stock_base.py @@ -0,0 +1,90 @@ +# flake8: noqa +""" +# 追加/更新股票基础信息 +""" + +import os +import sys +import json +from typing import Any +from collections import OrderedDict +import pandas as pd + +vnpy_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) +if vnpy_root not in sys.path: + sys.path.append(vnpy_root) + +os.environ["VNPY_TESTING"] = "1" +import baostock as bs +from vnpy.trader.constant import Exchange +from vnpy.trader.utility import load_json, load_data_from_pkb2, save_data_to_pkb2 +from vnpy.data.tdx.tdx_common import get_stock_type +import baostock as bs + +stock_type_map = { + "1": '股票', "2": "指数", "3": "其他" +} +STOCK_BASE_FILE = 'stock_base.pkb2' + + +def get_stock_base(): + """ 获取股票基础信息""" + base_file_name = os.path.abspath(os.path.join(os.path.dirname(__file__), STOCK_BASE_FILE)) + + base_data = load_data_from_pkb2(base_file_name) + if base_data is None: + return update_stock_base() + else: + return base_data + + +def update_stock_base(): + """ + 更新股票基础信息 + :return: + """ + base_file_name = os.path.abspath(os.path.join(os.path.dirname(__file__), STOCK_BASE_FILE)) + + base_data = load_data_from_pkb2(base_file_name) + + if base_data is None: + base_data = dict() + + login_msg = bs.login() + if login_msg.error_code != '0': + print(f'证券宝登录错误代码:{login_msg.error_code}, 错误信息:{login_msg.error_msg}') + return base_data + + rs = bs.query_stock_basic() + if rs.error_code != '0': + print(f'证券宝获取沪深A股历史K线数据错误代码:{rs.error_code}, 错误信息:{rs.error_msg}') + return + + # [dict] => dataframe + + print(f'返回字段:{rs.fields}') + while (rs.error_code == '0') and rs.next(): + row = rs.get_row_data() + exchange_code, stock_code = row[0].split('.') + exchange = Exchange.SSE if exchange_code == 'sh' else Exchange.SZSE + d = { + 'exchange': exchange.value, + 'code': stock_code, + 'name': row[1], + 'ipo_date': row[2], + 'out_date': row[3], + '类型': stock_type_map.get(row[4], '其他'), + 'type': get_stock_type(stock_code), + 'status': '上市' if row[5] == '1' else '退市' + } + base_data.update({f'{stock_code}.{exchange.value}': d}) + # print(f'{d}') + + save_data_to_pkb2(base_data, base_file_name) + print(f'更新完毕') + + return base_data + + +if __name__ == '__main__': + update_stock_base() diff --git a/vnpy/data/stock/stock_dividend.py b/vnpy/data/stock/stock_dividend.py new file mode 100644 index 00000000..121d0b3e --- /dev/null +++ b/vnpy/data/stock/stock_dividend.py @@ -0,0 +1,101 @@ +# flake8: noqa +""" +# 追加/更新股票除权除息 +""" + +import os +import sys +import json +from typing import Any +from collections import OrderedDict +import pandas as pd + +vnpy_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) +if vnpy_root not in sys.path: + sys.path.append(vnpy_root) + +os.environ["VNPY_TESTING"] = "1" +import baostock as bs +from vnpy.trader.constant import Exchange +from vnpy.trader.utility import load_json, load_data_from_pkb2, save_data_to_pkb2, extract_vt_symbol +from vnpy.data.tdx.tdx_common import get_stock_type +from vnpy.data.stock.stock_base import get_stock_base +import baostock as bs +import pandas as pd + +STOCK_DIVIDEND_FILE = 'stock_dividend.csv' +years = [str(y) for y in range(2006, 2020)] + +def update_stock_devidend(): + """ + 更新股票除权除息信息 + :return: + """ + stocks_data = get_stock_base() + + login_msg = bs.login() + if login_msg.error_code != '0': + print(f'证券宝登录错误代码:{login_msg.error_code}, 错误信息:{login_msg.error_msg}') + return + + result_list = [] + + for k, v in stocks_data.items(): + if v.get('类型') != '股票': + continue + symbol, exchange = extract_vt_symbol(k) + bs_code = '.'.join(['sh' if exchange == Exchange.SSE else 'sz', symbol]) + stock_name = v.get('name') + + print(f'开始获取{stock_name} {bs_code}得除权除息') + + for year in years: + rs = bs.query_dividend_data( + code=bs_code, + year=year + ) + if rs.error_code != '0': + print(f'证券宝获取沪深A股除权除息数据,错误代码:{rs.error_code}, 错误信息:{rs.error_msg}') + continue + + # [dict] => dataframe + + #print(f'返回字段:{rs.fields}') + while (rs.error_code == '0') and rs.next(): + row = rs.get_row_data() + exchange_code, stock_code = row[0].split('.') + exchange = Exchange.SSE if exchange_code == 'sh' else Exchange.SZSE + d = { + 'exchange': exchange.value, + 'code': stock_code, + 'name': stock_name, + 'dividPreNoticeDate': row[1], # 预批露公告日 + 'dividAgmPumDate': row[2], # 股东大会公告日期 + 'dividPlanAnnounceDate': row[3], # 预案公告日 + 'dividPlanDate': row[4], # 分红实施公告日 + 'dividRegistDate': row[5], # 股权登记告日 + 'dividOperateDate': row[6], # 除权除息日期 + 'dividPayDate': row[7], # 派息日 + 'dividStockMarketDate': row[8], # 红股上市交易日 + 'dividCashPsBeforeTax': row[9], # 每股股利税前 + 'dividCashPsAfterTax': row[10], # 每股股利税后 + 'dividStocksPs': row[11], # 每股红股 + 'dividCashStock': row[12], # 分红送转 + 'dividReserveToStockPs': row[13] # 每股转增资本 + } + result_list.append(d) + + print(f'{d}') + + if len(result_list) > 0: + df = pd.DataFrame(result_list) + + export_file_name = os.path.abspath(os.path.join(os.path.dirname(__file__), STOCK_DIVIDEND_FILE)) + + df.to_csv(export_file_name) + + print(f'保存除权除息至文件:{export_file_name}') + + +if __name__ == '__main__': + update_stock_devidend()