├── .flake8 ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_tushare_datafeed.py └── vnpy_tushare ├── __init__.py └── tushare_datafeed.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = venv,build,__pycache__,__init__.py,ib,talib,uic 3 | ignore = 4 | E501 line too long, fixed by black 5 | W503 line break before binary operator 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.4.7.0版本 2 | 3 | 1. 增加单元测试用例 4 | 2. to_ts_asset函数增加ETF基金支持 5 | 6 | # 1.2.89.0版本 7 | 1. 增加output函数用于输出日志 8 | 9 | # 1.2.85.1版本 10 | 1. 修复郑商所合约数据下载失败的问题 11 | 12 | # 1.2.85.0版本 13 | 1. 使用zoneinfo替换pytz实现时区功能 14 | 15 | # 1.2.64.3版本 16 | 17 | 1. 优化对查询数据失败的容错支持 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present, Xiaoyou Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VeighNa框架的TuShare数据服务接口 2 | 3 |

4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 |

13 | 14 | ## 说明 15 | 16 | 基于tushare模块的1.4.7版本开发,支持以下中国金融市场的K线数据: 17 | 18 | * 期货: 19 | * CFFEX:中国金融期货交易所 20 | * SHFE:上海期货交易所 21 | * DCE:大连商品交易所 22 | * CZCE:郑州商品交易所 23 | * INE:上海国际能源交易中心 24 | * 股票: 25 | * SSE:上海证券交易所 26 | * SZSE:深圳证券交易所 27 | * BSE:北京证券交易所 28 | 29 | 注意:需要使用相应的数据服务权限,可以通过[该页面](https://www.tushare.pro)注册使用。 30 | 31 | ## 数据使用事项 32 | 33 | tushare数据源期货数据中,第一条夜盘k线数据是集合竞价数据,用户可以根据自己需求进行过滤或者合并。 34 | 35 | ## 安装 36 | 37 | 安装环境推荐基于3.9.0版本以上的【[**VeighNa Studio**](https://www.vnpy.com)】。 38 | 39 | 直接使用pip命令: 40 | 41 | ``` 42 | pip install vnpy_tushare 43 | ``` 44 | 45 | 46 | 或者下载源代码后,解压后在cmd中运行: 47 | 48 | ``` 49 | pip install . 50 | ``` 51 | 52 | 53 | ## 使用 54 | 55 | 在VeighNa中使用TuShare时,需要在全局配置中填写以下字段信息: 56 | 57 | |名称|含义|必填|举例| 58 | |---------|----|---|---| 59 | |datafeed.name|名称|是|tushare| 60 | |datafeed.username|用户名|否|token| 61 | |datafeed.password|密码|是|c3a110417f08f26d2c221edc0c50d4a8a5001502eea89cf5| 62 | 63 | 64 | # 单元测试 65 | 66 | 单元测试代码目录为`./tests/test_*.py` 67 | ```sh 68 | # 指定文件执行单元测试 69 | python -m unittest tests\test_your_file_name.py 70 | 71 | # 全量执行 72 | python -m unittest 73 | ``` 74 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = vnpy_tushare 3 | version = 1.4.7.0 4 | url = https://www.vnpy.com 5 | license = MIT 6 | author = Xiaoyou Chen 7 | author_email = xiaoyou.chen@mail.vnpy.com 8 | description = Tushare datafeed for VeighNa quant trading framework. 9 | long_description = file: README.md 10 | long_description_content_type = text/markdown 11 | keywords = 12 | quant 13 | quantitative 14 | investment 15 | trading 16 | algotrading 17 | classifiers = 18 | Development Status :: 5 - Production/Stable 19 | Operating System :: OS Independent 20 | Programming Language :: Python :: 3 21 | Programming Language :: Python :: 3.10 22 | Programming Language :: Python :: 3.11 23 | Programming Language :: Python :: 3.12 24 | Topic :: Office/Business :: Financial :: Investment 25 | Programming Language :: Python :: Implementation :: CPython 26 | License :: OSI Approved :: MIT License 27 | Natural Language :: Chinese (Simplified) 28 | 29 | [options] 30 | packages = find: 31 | include_package_data = True 32 | zip_safe = False 33 | install_requires = 34 | tushare 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vnpy/vnpy_tushare/aa815f150f9621f848ba2b2f8a66611e2448ca25/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_tushare_datafeed.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from vnpy_tushare.tushare_datafeed import to_ts_asset 4 | from vnpy.trader.constant import Exchange 5 | 6 | 7 | class TestToTsAsset(unittest.TestCase): 8 | # 股票 9 | def test_stock(self): 10 | self.assertEqual(to_ts_asset('600009', Exchange.SSE), "E") # 沪市 11 | self.assertEqual(to_ts_asset('688981', Exchange.SSE), "E") # 科创版 12 | self.assertEqual(to_ts_asset('000001', Exchange.SZSE), "E") # 深市 13 | self.assertEqual(to_ts_asset('300308', Exchange.SZSE), "E") # 创业板 14 | self.assertEqual(to_ts_asset('430418', Exchange.BSE), "E") # 北交所 15 | self.assertEqual(to_ts_asset('835305', Exchange.BSE), "E") # 北交所 16 | 17 | # 指数 18 | def test_index(self): 19 | self.assertEqual(to_ts_asset('000001', Exchange.SSE), "I") # 上证指数 20 | self.assertEqual(to_ts_asset('000688', Exchange.SSE), "I") # 科创版指 21 | self.assertEqual(to_ts_asset('399001', Exchange.SZSE), "I") # 深证指数 22 | self.assertEqual(to_ts_asset('399006', Exchange.SZSE), "I") # 创业板指 23 | self.assertEqual(to_ts_asset('899050', Exchange.BSE), "I") # 北交所50 24 | 25 | # 基金 26 | def test_fund(self): 27 | self.assertEqual(to_ts_asset('159934', Exchange.SZSE), "FD") # 深市etf 28 | self.assertEqual(to_ts_asset('518880', Exchange.SSE), "FD") # 沪市etf 29 | 30 | # 期货 31 | def test_future(self): 32 | self.assertEqual(to_ts_asset('i2409', Exchange.CFFEX), "FT") 33 | 34 | 35 | if __name__ == '__main__': 36 | unittest.main() 37 | -------------------------------------------------------------------------------- /vnpy_tushare/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015-present, Xiaoyou Chen 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | import importlib_metadata 25 | 26 | from .tushare_datafeed import TushareDatafeed as Datafeed 27 | 28 | 29 | try: 30 | __version__ = importlib_metadata.version("vnpy_tushare") 31 | except importlib_metadata.PackageNotFoundError: 32 | __version__ = "dev" 33 | -------------------------------------------------------------------------------- /vnpy_tushare/tushare_datafeed.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, datetime 2 | from typing import Optional, Callable 3 | from copy import deepcopy 4 | import re 5 | 6 | import pandas as pd 7 | from pandas import DataFrame 8 | import tushare as ts 9 | from tushare.pro.client import DataApi 10 | 11 | from vnpy.trader.setting import SETTINGS 12 | from vnpy.trader.datafeed import BaseDatafeed 13 | from vnpy.trader.constant import Exchange, Interval 14 | from vnpy.trader.object import BarData, HistoryRequest 15 | from vnpy.trader.utility import round_to, ZoneInfo 16 | 17 | # 数据频率映射 18 | INTERVAL_VT2TS: dict[Interval, str] = { 19 | Interval.MINUTE: "1min", 20 | Interval.HOUR: "60min", 21 | Interval.DAILY: "D", 22 | } 23 | 24 | # 股票支持列表 25 | STOCK_LIST: list[Exchange] = [ 26 | Exchange.SSE, 27 | Exchange.SZSE, 28 | Exchange.BSE, 29 | ] 30 | 31 | # 期货支持列表 32 | FUTURE_LIST: list[Exchange] = [ 33 | Exchange.CFFEX, 34 | Exchange.SHFE, 35 | Exchange.CZCE, 36 | Exchange.DCE, 37 | Exchange.INE, 38 | Exchange.GFEX 39 | ] 40 | 41 | # 交易所映射 42 | EXCHANGE_VT2TS: dict[Exchange, str] = { 43 | Exchange.CFFEX: "CFX", 44 | Exchange.SHFE: "SHF", 45 | Exchange.CZCE: "ZCE", 46 | Exchange.DCE: "DCE", 47 | Exchange.INE: "INE", 48 | Exchange.SSE: "SH", 49 | Exchange.SZSE: "SZ", 50 | Exchange.BSE: "BJ", 51 | Exchange.GFEX: "GFE" 52 | } 53 | 54 | # 时间调整映射 55 | INTERVAL_ADJUSTMENT_MAP: dict[Interval, timedelta] = { 56 | Interval.MINUTE: timedelta(minutes=1), 57 | Interval.HOUR: timedelta(hours=1), 58 | Interval.DAILY: timedelta() 59 | } 60 | 61 | # 中国上海时区 62 | CHINA_TZ = ZoneInfo("Asia/Shanghai") 63 | 64 | 65 | def to_ts_symbol(symbol, exchange) -> Optional[str]: 66 | """将交易所代码转换为tushare代码""" 67 | # 股票 68 | if exchange in STOCK_LIST: 69 | ts_symbol: str = f"{symbol}.{EXCHANGE_VT2TS[exchange]}" 70 | # 期货 71 | elif exchange in FUTURE_LIST: 72 | if exchange is not Exchange.CZCE: 73 | ts_symbol: str = f"{symbol}.{EXCHANGE_VT2TS[exchange]}".upper() 74 | else: 75 | for count, word in enumerate(symbol): 76 | if word.isdigit(): 77 | break 78 | 79 | year: str = symbol[count] 80 | month: str = symbol[count + 1:] 81 | if year == "9": 82 | year = "1" + year 83 | else: 84 | year = "2" + year 85 | 86 | product: str = symbol[:count] 87 | ts_symbol: str = f"{product}{year}{month}.ZCE".upper() 88 | else: 89 | return None 90 | 91 | return ts_symbol 92 | 93 | 94 | def to_ts_asset(symbol, exchange) -> Optional[str]: 95 | """生成tushare资产类别""" 96 | # 股票 97 | if exchange in STOCK_LIST: 98 | if exchange is Exchange.SSE and symbol[0] == "6": 99 | asset: str = "E" 100 | elif exchange is Exchange.SSE and symbol[0] == "5": 101 | asset: str = "FD" # 场内etf 102 | elif exchange is Exchange.SZSE and symbol[0] == "1": 103 | asset: str = "FD" # 场内etf 104 | # 39开头是指数,比如399001 105 | elif exchange is Exchange.SZSE and re.search("^(0|3)", symbol) and not symbol.startswith('39'): 106 | asset: str = "E" 107 | # 89开头是指数,比如899050 108 | elif exchange is Exchange.BSE and not symbol.startswith('89'): 109 | asset: str = "E" 110 | else: 111 | asset: str = "I" 112 | # 期货 113 | elif exchange in FUTURE_LIST: 114 | asset: str = "FT" 115 | else: 116 | return None 117 | 118 | return asset 119 | 120 | 121 | class TushareDatafeed(BaseDatafeed): 122 | """TuShare数据服务接口""" 123 | 124 | def __init__(self): 125 | """""" 126 | self.username: str = SETTINGS["datafeed.username"] 127 | self.password: str = SETTINGS["datafeed.password"] 128 | 129 | self.inited: bool = False 130 | 131 | def init(self, output: Callable = print) -> bool: 132 | """初始化""" 133 | if self.inited: 134 | return True 135 | 136 | if not self.username: 137 | output("Tushare数据服务初始化失败:用户名为空!") 138 | return False 139 | 140 | if not self.password: 141 | output("Tushare数据服务初始化失败:密码为空!") 142 | return False 143 | 144 | ts.set_token(self.password) 145 | self.pro: Optional[DataApi] = ts.pro_api() 146 | self.inited = True 147 | 148 | return True 149 | 150 | def query_bar_history(self, req: HistoryRequest, output: Callable = print) -> Optional[list[BarData]]: 151 | """查询k线数据""" 152 | if not self.inited: 153 | self.init(output) 154 | 155 | symbol: str = req.symbol 156 | exchange: Exchange = req.exchange 157 | interval: Interval = req.interval 158 | start: datetime = req.start.strftime("%Y-%m-%d %H:%M:%S") 159 | end: datetime = req.end.strftime("%Y-%m-%d %H:%M:%S") 160 | 161 | ts_symbol: str = to_ts_symbol(symbol, exchange) 162 | if not ts_symbol: 163 | return None 164 | 165 | asset: str = to_ts_asset(symbol, exchange) 166 | if not asset: 167 | return None 168 | 169 | ts_interval: str = INTERVAL_VT2TS.get(interval) 170 | if not ts_interval: 171 | return None 172 | 173 | adjustment: timedelta = INTERVAL_ADJUSTMENT_MAP[interval] 174 | 175 | try: 176 | d1: DataFrame = ts.pro_bar( 177 | ts_code=ts_symbol, 178 | start_date=start, 179 | end_date=end, 180 | asset=asset, 181 | freq=ts_interval 182 | ) 183 | except IOError as ex: 184 | output(f"发生输入/输出错误:{ex.strerror}") 185 | return [] 186 | 187 | df: DataFrame = deepcopy(d1) 188 | 189 | while True: 190 | if len(d1) != 8000: 191 | break 192 | tmp_end: str = d1["trade_time"].values[-1] 193 | 194 | d1 = ts.pro_bar( 195 | ts_code=ts_symbol, 196 | start_date=start, 197 | end_date=tmp_end, 198 | asset=asset, 199 | freq=ts_interval 200 | ) 201 | df = pd.concat([df[:-1], d1]) 202 | 203 | bar_keys: list[datetime] = [] 204 | bar_dict: dict[datetime, BarData] = {} 205 | data: list[BarData] = [] 206 | 207 | # 处理原始数据中的NaN值 208 | df.fillna(0, inplace=True) 209 | 210 | if df is not None: 211 | for ix, row in df.iterrows(): 212 | if row["open"] is None: 213 | continue 214 | 215 | if interval.value == "d": 216 | dt: str = row["trade_date"] 217 | dt: datetime = datetime.strptime(dt, "%Y%m%d") 218 | else: 219 | dt: str = row["trade_time"] 220 | dt: datetime = datetime.strptime(dt, "%Y-%m-%d %H:%M:%S") - adjustment 221 | 222 | dt = dt.replace(tzinfo=CHINA_TZ) 223 | 224 | turnover = row.get("amount", 0) 225 | if turnover is None: 226 | turnover = 0 227 | 228 | open_interest = row.get("oi", 0) 229 | if open_interest is None: 230 | open_interest = 0 231 | 232 | bar: BarData = BarData( 233 | symbol=symbol, 234 | exchange=exchange, 235 | interval=interval, 236 | datetime=dt, 237 | open_price=round_to(row["open"], 0.000001), 238 | high_price=round_to(row["high"], 0.000001), 239 | low_price=round_to(row["low"], 0.000001), 240 | close_price=round_to(row["close"], 0.000001), 241 | volume=row["vol"], 242 | turnover=turnover, 243 | open_interest=open_interest, 244 | gateway_name="TS" 245 | ) 246 | 247 | bar_dict[dt] = bar 248 | 249 | bar_keys: list = bar_dict.keys() 250 | bar_keys = sorted(bar_keys, reverse=False) 251 | for i in bar_keys: 252 | data.append(bar_dict[i]) 253 | 254 | return data 255 | --------------------------------------------------------------------------------