├── .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 |
--------------------------------------------------------------------------------