├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── run_gm_web.py ├── run_jq_web.py ├── run_tq_web.py ├── run_ts_web.py └── web ├── css ├── app.0ead0cf7.css └── chunk-vendors.e818cfcf.css ├── fonts ├── element-icons.535877f5.woff └── element-icons.732389de.ttf ├── index.html └── js ├── app.fdd123f3.js ├── app.fdd123f3.js.map ├── chunk-vendors.aefc5aaa.js └── chunk-vendors.aefc5aaa.js.map /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea/ 6 | conf.py 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 zengbin93 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 | # 缠中说禅技术分析工具网页 2 | 3 | 使用 Tushare Pro / 聚宽 / 掘金 / 天勤 的数据进行缠中说禅技术分析结果展示 4 | 5 | ## 启动方法 6 | 7 | 1. 执行 `pip install -r requirements.txt` 安装环境 8 | 2. 确定所要使用的数据源,完成相关设置 9 | 3. 执行对应的数据源的脚本启动服务,不要改端口!!! 10 | 11 | `python run_ts_web.py` - 启动使用 `Tushare Pro` 数据的前端页面 12 | 13 | `python run_jq_web.py` - 启动使用 `聚宽` 数据的前端页面 14 | 15 | `python run_gm_web.py` - 启动使用 `掘金` 数据的前端页面 16 | 17 | `python run_tq_web.py` - 启动使用 `天勤` 数据的前端页面 18 | 19 | 启动后在本地 8005 端口访问服务,在对应的脚本中可以看到示例; 20 | 其中,ts_code 为对应的标的代码;trade_date 为交易日期,freqs 为K线周期 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqsdk 2 | flask 3 | tushare 4 | requests 5 | pandas 6 | tornado 7 | czsc 8 | gm 9 | -------------------------------------------------------------------------------- /run_gm_web.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import json 3 | import os 4 | from tornado.ioloop import IOLoop 5 | from tornado.httpserver import HTTPServer 6 | from tornado.options import define, parse_command_line, options 7 | from tornado.web import RequestHandler, Application 8 | from tornado.web import StaticFileHandler 9 | from datetime import datetime, timedelta 10 | import czsc 11 | from czsc import KlineAnalyze 12 | from gm.api import * 13 | 14 | 15 | # 在这里设置你的掘金 token,要在本地启动掘金终端,才能正常获取数据 16 | set_token("set your gm token") 17 | 18 | assert czsc.__version__ == "0.5.8", "当前 czsc 版本为 {},请升级为 0.5.8 版本".format(czsc.__version__) 19 | 20 | 21 | def get_gm_kline(symbol, end_date, freq='D', k_count=3000): 22 | """从掘金获取历史K线数据""" 23 | if "-" not in end_date and isinstance(end_date, str): 24 | end_date = datetime.strptime(end_date, "%Y%m%d") 25 | freq_convert = {"60s": "1min", "300s": "5min", "1800s": "30min", "3600s": "60min", "1d": "D"} 26 | freq_convert = {v: k for k, v in freq_convert.items()} 27 | if freq[-1] in ['n', 'D']: 28 | freq = freq_convert[freq] 29 | if freq.endswith('min'): 30 | end_date += timedelta(days=1) 31 | df = history_n(symbol=symbol, frequency=freq, end_time=end_date, 32 | fields='symbol,eob,open,close,high,low,volume', 33 | count=k_count, df=True) 34 | df['dt'] = df['eob'] 35 | df['vol'] = df['volume'] 36 | df = df[['symbol', 'dt', 'open', 'close', 'high', 'low', 'vol']] 37 | df.sort_values('dt', inplace=True, ascending=True) 38 | df['dt'] = df.dt.apply(lambda x: x.strftime(r"%Y-%m-%d %H:%M:%S")) 39 | df.reset_index(drop=True, inplace=True) 40 | 41 | for col in ['open', 'close', 'high', 'low']: 42 | df[col] = df[col].apply(round, args=(2,)) 43 | return df 44 | 45 | 46 | # 端口固定为 8005,不可以调整 47 | define('port', type=int, default=8005, help='服务器端口') 48 | current_path = os.path.dirname(__file__) 49 | 50 | 51 | class BaseHandler(RequestHandler): 52 | def set_default_headers(self): 53 | self.set_header("Access-Control-Allow-Origin", "*") # 这个地方可以写域名 54 | self.set_header("Access-Control-Allow-Headers", "x-requested-with") 55 | self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') 56 | 57 | def post(self): 58 | self.write('some post') 59 | 60 | def get(self): 61 | self.write('some get') 62 | 63 | def options(self): 64 | self.set_status(204) 65 | self.finish() 66 | 67 | 68 | class BasicHandler(BaseHandler): 69 | """股票基本信息""" 70 | def get(self): 71 | ts_code = self.get_argument('ts_code') 72 | results = {"msg": "success", "basic": None} 73 | self.write(json.dumps(results, ensure_ascii=False)) 74 | 75 | 76 | class KlineHandler(BaseHandler): 77 | """K 线""" 78 | def get(self): 79 | ts_code = self.get_argument('ts_code') 80 | freq = self.get_argument('freq') 81 | trade_date = self.get_argument('trade_date') 82 | if trade_date == 'null': 83 | trade_date = datetime.now().date().__str__().replace("-", "") 84 | kline = get_gm_kline(symbol=ts_code, end_date=trade_date, freq=freq, k_count=3000) 85 | ka = KlineAnalyze(kline, bi_mode="new", verbose=False, use_xd=True, max_count=5000) 86 | kline = ka.to_df(ma_params=(5, 20), use_macd=True, max_count=5000, mode='new') 87 | kline = kline.fillna("") 88 | kline.loc[:, "dt"] = kline.dt.apply(str) 89 | columns = ["dt", "open", "close", "low", "high", "vol", 'fx_mark', 'fx', 'bi', 'xd'] 90 | 91 | self.finish({'kdata': kline[columns].values.tolist()}) 92 | 93 | 94 | if __name__ == '__main__': 95 | parse_command_line() 96 | app = Application([ 97 | ('/kline', KlineHandler), 98 | ('/basic', BasicHandler), 99 | (r'^/(.*?)$', StaticFileHandler, {"path": os.path.join(current_path, "web"), 100 | "default_filename": "index.html"}), 101 | ], 102 | static_path=os.path.join(current_path, "web"), 103 | dubug=True 104 | ) 105 | http_server = HTTPServer(app) 106 | http_server.listen(options.port) 107 | IOLoop.current().start() 108 | 109 | # 交易所代码如下: 110 | # 上交所 SHSE 111 | # 深交所 SZSE 112 | # 中金所 CFFEX 113 | # 上期所 SHFE 114 | # 大商所 DCE 115 | # 郑商所 CZCE 116 | # 上海国际能源交易中心 INE 117 | 118 | # 掘金数据文档:https://www.myquant.cn/docs/data/98? 119 | 120 | # http://localhost:8005/?ts_code=SHSE.000001&asset=I&trade_date=20200613&freqs=D,30min,5min,1min 121 | -------------------------------------------------------------------------------- /run_jq_web.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from tornado.ioloop import IOLoop 3 | from tornado.httpserver import HTTPServer 4 | from tornado.options import define, options, parse_command_line 5 | from tornado.web import RequestHandler, Application 6 | from tornado.web import StaticFileHandler 7 | from czsc import KlineAnalyze 8 | import os 9 | import pickle 10 | import czsc 11 | import json 12 | import requests 13 | import warnings 14 | import pandas as pd 15 | from datetime import datetime 16 | 17 | url = "https://dataapi.joinquant.com/apis" 18 | home_path = os.path.expanduser("~") 19 | file_token = os.path.join(home_path, "jq.token") 20 | 21 | assert czsc.__version__ == "0.5.8", "当前 czsc 版本为 {},请升级为 0.5.8 版本".format(czsc.__version__) 22 | 23 | def set_token(jq_mob, jq_pwd): 24 | """ 25 | 26 | :param jq_mob: str 27 | mob是申请JQData时所填写的手机号 28 | :param jq_pwd: str 29 | Password为聚宽官网登录密码,新申请用户默认为手机号后6位 30 | :return: 31 | """ 32 | pickle.dump([jq_mob, jq_pwd], open(file_token, 'wb')) 33 | 34 | 35 | def get_token(): 36 | """获取调用凭证""" 37 | if not os.path.exists(file_token): 38 | raise ValueError(f"{file_token} 文件不存在,请先调用 set_token 进行设置") 39 | 40 | jq_mob, jq_pwd = pickle.load(open(file_token, 'rb')) 41 | body = { 42 | "method": "get_current_token", 43 | "mob": jq_mob, # mob是申请JQData时所填写的手机号 44 | "pwd": jq_pwd, # Password为聚宽官网登录密码,新申请用户默认为手机号后6位 45 | } 46 | response = requests.post(url, data=json.dumps(body)) 47 | token = response.text 48 | return token 49 | 50 | 51 | def text2df(text): 52 | rows = [x.split(",") for x in text.strip().split('\n')] 53 | df = pd.DataFrame(rows[1:], columns=rows[0]) 54 | return df 55 | 56 | 57 | def get_kline(symbol, end_date: datetime, freq: str, start_date: datetime = None, count=None): 58 | """获取K线数据 59 | 60 | :param symbol: str 61 | 聚宽标的代码 62 | :param start_date: datetime 63 | 截止日期 64 | :param end_date: datetime 65 | 截止日期 66 | :param freq: str 67 | K线级别 68 | :param count: int 69 | K线数量,最大值为 5000 70 | :return: pd.DataFrame 71 | 72 | >>> start_date = datetime.strptime("20200701", "%Y%m%d") 73 | >>> end_date = datetime.strptime("20200719", "%Y%m%d") 74 | >>> df1 = get_kline(symbol="000001.XSHG", start_date=start_date, end_date=end_date, freq="1min") 75 | >>> df2 = get_kline(symbol="000001.XSHG", end_date=end_date, freq="1min", count=1000) 76 | """ 77 | if count and count > 5000: 78 | warnings.warn(f"count={count}, 超过5000的最大值限制,仅返回最后5000条记录") 79 | 80 | # 1m, 5m, 15m, 30m, 60m, 120m, 1d, 1w, 1M 81 | freq_convert = {"1min": "1m", "5min": '5m', '15min': '15m', 82 | "30min": "30m", "60min": '60m', "D": "1d", "W": '1w'} 83 | if start_date: 84 | data = { 85 | "method": "get_price_period", 86 | "token": get_token(), 87 | "code": symbol, 88 | "unit": freq_convert[freq], 89 | "date": start_date.strftime("%Y-%m-%d"), 90 | "end_date": end_date.strftime("%Y-%m-%d"), 91 | # "fq_ref_date": end_date 92 | } 93 | elif count: 94 | data = { 95 | "method": "get_price", 96 | "token": get_token(), 97 | "code": symbol, 98 | "count": count, 99 | "unit": freq_convert[freq], 100 | "end_date": end_date.strftime("%Y-%m-%d"), 101 | # "fq_ref_date": end_date 102 | } 103 | else: 104 | raise ValueError("start_date 和 count 不能同时为空") 105 | 106 | r = requests.post(url, data=json.dumps(data)) 107 | df = text2df(r.text) 108 | df['symbol'] = symbol 109 | df.rename({'date': 'dt', 'volume': 'vol'}, axis=1, inplace=True) 110 | df = df[['symbol', 'dt', 'open', 'close', 'high', 'low', 'vol']] 111 | for col in ['open', 'close', 'high', 'low', 'vol']: 112 | df.loc[:, col] = df[col].apply(lambda x: round(float(x), 2)) 113 | df.loc[:, "dt"] = pd.to_datetime(df['dt']) 114 | return df 115 | 116 | 117 | define('port', type=int, default=8005, help='服务器端口') 118 | current_path = os.path.dirname(__file__) 119 | 120 | 121 | class BaseHandler(RequestHandler): 122 | def set_default_headers(self): 123 | self.set_header("Access-Control-Allow-Origin", "*") # 这个地方可以写域名 124 | self.set_header("Access-Control-Allow-Headers", "x-requested-with") 125 | self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') 126 | 127 | def post(self): 128 | self.write('some post') 129 | 130 | def get(self): 131 | self.write('some get') 132 | 133 | def options(self): 134 | self.set_status(204) 135 | self.finish() 136 | 137 | 138 | class BasicHandler(BaseHandler): 139 | """股票基本信息""" 140 | def get(self): 141 | ts_code = self.get_argument('ts_code') 142 | results = {"msg": "success", "basic": None} 143 | self.write(json.dumps(results, ensure_ascii=False)) 144 | 145 | 146 | class KlineHandler(BaseHandler): 147 | """K 线""" 148 | def get(self): 149 | ts_code = self.get_argument('ts_code') 150 | freq = self.get_argument('freq') 151 | trade_date = self.get_argument('trade_date') 152 | if trade_date == 'null': 153 | trade_date = datetime.now().date() 154 | else: 155 | trade_date = datetime.strptime(trade_date, "%Y%m%d") 156 | kline = get_kline(symbol=ts_code, end_date=trade_date, freq=freq, count=5000) 157 | kline.loc[:, "dt"] = pd.to_datetime(kline.dt) 158 | ka = KlineAnalyze(kline, bi_mode="new", verbose=False, use_xd=True, max_count=5000) 159 | kline = ka.to_df(ma_params=(5, 20), use_macd=True, max_count=5000, mode='new') 160 | kline = kline.fillna("") 161 | kline.loc[:, "dt"] = kline.dt.apply(str) 162 | columns = ["dt", "open", "close", "low", "high", "vol", 'fx_mark', 'fx', 'bi', 'xd'] 163 | 164 | self.finish({'kdata': kline[columns].values.tolist()}) 165 | 166 | 167 | if __name__ == '__main__': 168 | parse_command_line() 169 | app = Application([ 170 | ('/kline', KlineHandler), 171 | ('/basic', BasicHandler), 172 | (r'^/(.*?)$', StaticFileHandler, {"path": os.path.join(current_path, "web"), 173 | "default_filename": "index.html"}), 174 | ], 175 | static_path=os.path.join(current_path, "web"), 176 | dubug=True 177 | ) 178 | http_server = HTTPServer(app) 179 | http_server.listen(options.port) 180 | IOLoop.current().start() 181 | 182 | # 查看聚宽标的编码规范 183 | # https://www.joinquant.com/help/api/help?name=JQData#%E8%8E%B7%E5%8F%96%E6%A0%87%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BF%A1%E6%81%AF 184 | 185 | # 使用聚宽数据,只需要给出正确的标的代码,支持股票和期货,有实时数据,不需要设置 asset 参数 186 | # http://localhost:8005/?ts_code=000001.XSHG&trade_date=20201121&freqs=1min 187 | 188 | 189 | # 聚宽期货数据:https://www.joinquant.com/help/api/help?name=Future 190 | # 191 | # ``` 192 | # 交易市场 代码后缀 示例代码 证券简称 193 | # 上海证券交易所 .XSHG '600519.XSHG' 贵州茅台 194 | # 深圳证券交易所 .XSHE '000001.XSHE' 平安银行 195 | # 中金所 .CCFX 'IC9999.CCFX' 中证500主力合约 196 | # 大商所 .XDCE 'A9999.XDCE' 豆一主力合约 197 | # 上期所 .XSGE 'AU9999.XSGE' 黄金主力合约 198 | # 郑商所 .XZCE 'CY8888.XZCE' 棉纱期货指数 199 | # 上海能源交易所 .XINE 'SC9999.XINE' 原油主力合约 200 | # ``` 201 | 202 | -------------------------------------------------------------------------------- /run_tq_web.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | 天勤开始收费,不再维护这个脚本 4 | ================================================= 5 | """ 6 | import os 7 | import json 8 | import pandas as pd 9 | import czsc 10 | from czsc import KlineAnalyze 11 | from datetime import datetime, timedelta 12 | from flask import Flask, request, make_response, jsonify 13 | from tqsdk import TqApi 14 | 15 | 16 | base_path = os.path.split(os.path.realpath(__file__))[0] 17 | web_path = os.path.join(base_path, 'web') 18 | app = Flask(__name__, static_folder=web_path) 19 | 20 | # api = TqApi(_stock=True) # 支持股票数据 21 | api = TqApi() 22 | 23 | 24 | def format_kline(kline): 25 | """格式化K线""" 26 | def __convert_time(t): 27 | try: 28 | dt = datetime.utcfromtimestamp(t/1000000000) 29 | dt = dt + timedelta(hours=8) # 中国默认时区 30 | return dt 31 | except: 32 | return "" 33 | 34 | kline['dt'] = kline['datetime'].apply(__convert_time) 35 | kline['vol'] = kline['volume'] 36 | columns = ['symbol', 'dt', 'open', 'close', 'high', 'low', 'vol'] 37 | df = kline[columns] 38 | df = df.dropna(axis=0) 39 | df.drop_duplicates(subset=['dt'], inplace=True) 40 | df.sort_values('dt', inplace=True, ascending=True) 41 | df.reset_index(drop=True, inplace=True) 42 | return df 43 | 44 | 45 | def get_kline(symbol="SHFE.cu2002", freq='1min', k_count=3000): 46 | """获取K线""" 47 | freq_map = {'1min': 60, '5min': 300, '15min': 900, '30min': 1800, 48 | '60min': 3600, 'D': 3600*24, 'W': 86400*7} 49 | df = api.get_kline_serial(symbol, duration_seconds=freq_map[freq], data_length=k_count) 50 | df = format_kline(df) 51 | return df 52 | 53 | 54 | @app.route('/', methods=['GET']) 55 | def index(): 56 | return app.send_static_file('index.html') 57 | 58 | 59 | @app.route('/kline', methods=['POST', 'GET']) 60 | def kline(): 61 | if request.method == "POST": 62 | data = json.loads(request.get_data(as_text=True)) 63 | elif request.method == "GET": 64 | data = request.args 65 | else: 66 | raise ValueError 67 | 68 | ts_code = data.get('ts_code') 69 | freq = data.get('freq') 70 | k = get_kline(symbol=ts_code, freq=freq, k_count=5000) 71 | if czsc.__version__ < "0.5": 72 | ka = KlineAnalyze(k, bi_mode="new", xd_mode='strict') 73 | k = pd.DataFrame(ka.kline_new) 74 | else: 75 | ka = KlineAnalyze(k, min_bi_k=5, verbose=False) 76 | k = ka.to_df(ma_params=(5, 20), use_macd=True, max_count=5000) 77 | 78 | k = k.fillna("") 79 | kline.loc[:, "dt"] = kline.dt.apply(str) 80 | columns = ["dt", "open", "close", "low", "high", "vol", 'fx_mark', 'fx', 'bi', 'xd'] 81 | res = make_response(jsonify({'kdata': k[columns].values.tolist()})) 82 | res.headers['Access-Control-Allow-Origin'] = '*' 83 | res.headers['Access-Control-Allow-Method'] = '*' 84 | res.headers['Access-Control-Allow-Headers'] = '*' 85 | return res 86 | 87 | 88 | if __name__ == '__main__': 89 | app.run(port=8005, debug=True) 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /run_ts_web.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import czsc 4 | import json 5 | import os 6 | import time 7 | import pandas as pd 8 | from tornado.ioloop import IOLoop 9 | from tornado.httpserver import HTTPServer 10 | from tornado.options import define, options, parse_command_line 11 | from tornado.web import RequestHandler, Application 12 | from tornado.web import StaticFileHandler 13 | import tushare as ts 14 | from datetime import datetime, timedelta 15 | import czsc 16 | from czsc import KlineAnalyze 17 | 18 | # 首次使用,需要在这里设置你的 tushare token,用于获取数据;在同一台机器上,tushare token 只需要设置一次 19 | # 没有 token,到 https://tushare.pro/register?reg=7 注册获取 20 | # ts.set_token("your tushare token") 21 | 22 | assert czsc.__version__ == "0.5.8", "当前 czsc 版本为 {},请升级为 0.5.8 版本".format(czsc.__version__) 23 | 24 | 25 | def _get_start_date(end_date, freq): 26 | end_date = datetime.strptime(end_date, '%Y%m%d') 27 | if freq == '1min': 28 | start_date = end_date - timedelta(days=60) 29 | elif freq == '5min': 30 | start_date = end_date - timedelta(days=150) 31 | elif freq == '30min': 32 | start_date = end_date - timedelta(days=1000) 33 | elif freq == 'D': 34 | start_date = end_date - timedelta(weeks=1000) 35 | elif freq == 'W': 36 | start_date = end_date - timedelta(weeks=1000) 37 | else: 38 | raise ValueError("'freq' value error, current value is %s, " 39 | "optional valid values are ['1min', '5min', '30min', " 40 | "'D', 'W']" % freq) 41 | return start_date 42 | 43 | 44 | def get_kline(ts_code, end_date, freq='30min', asset='E'): 45 | """获取指定级别的前复权K线 46 | 47 | :param ts_code: str 48 | 股票代码,如 600122.SH 49 | :param freq: str 50 | K线级别,可选值 [1min, 5min, 15min, 30min, 60min, D, M, Y] 51 | :param end_date: str 52 | 日期,如 20190610 53 | :param asset: str 54 | 交易资产类型,可选值 E股票 I沪深指数 C数字货币 FT期货 FD基金 O期权 CB可转债(v1.2.39),默认E 55 | :return: pd.DataFrame 56 | columns = ["symbol", "dt", "open", "close", "high", "low", "vol"] 57 | """ 58 | start_date = _get_start_date(end_date, freq) 59 | start_date = start_date.date().__str__().replace("-", "") 60 | end_date = datetime.strptime(end_date, '%Y%m%d') 61 | end_date = end_date + timedelta(days=1) 62 | end_date = end_date.date().__str__().replace("-", "") 63 | 64 | df = ts.pro_bar(ts_code=ts_code, freq=freq, start_date=start_date, end_date=end_date, 65 | adj='qfq', asset=asset) 66 | 67 | # 统一 k 线数据格式为 6 列,分别是 ["symbol", "dt", "open", "close", "high", "low", "vr"] 68 | if "min" in freq: 69 | df.rename(columns={'ts_code': "symbol", "trade_time": "dt"}, inplace=True) 70 | else: 71 | df.rename(columns={'ts_code': "symbol", "trade_date": "dt"}, inplace=True) 72 | 73 | df.drop_duplicates(subset='dt', keep='first', inplace=True) 74 | df.sort_values('dt', inplace=True) 75 | df['dt'] = df.dt.apply(str) 76 | if freq.endswith("min"): 77 | # 清理 9:30 的空数据 78 | df['not_start'] = df.dt.apply(lambda x: not x.endswith("09:30:00")) 79 | df = df[df['not_start']] 80 | df.reset_index(drop=True, inplace=True) 81 | 82 | k = df[['symbol', 'dt', 'open', 'close', 'high', 'low', 'vol']] 83 | 84 | for col in ['open', 'close', 'high', 'low']: 85 | k[col] = k[col].apply(round, args=(2,)) 86 | return k 87 | 88 | 89 | def get_stock_basic(ts_code=None): 90 | """获取股票的基本信息""" 91 | file_cache = "stock_basic.csv" 92 | if os.path.exists(file_cache) and time.time() - os.path.getmtime(file_cache) < 3600: 93 | df = pd.read_csv(file_cache, dtype={"list_date": str}, encoding="utf-8") 94 | else: 95 | pro = ts.pro_api() 96 | df = pro.stock_basic(exchange='', list_status='L', fields='ts_code,name,area,industry,list_date') 97 | df.to_csv(file_cache, index=False, encoding="utf-8") 98 | 99 | if not ts_code: 100 | return df 101 | else: 102 | share = df[df["ts_code"] == ts_code].iloc[0].to_dict() 103 | return share 104 | 105 | 106 | # 端口固定为 8005,不可以调整 107 | define('port', type=int, default=8005, help='服务器端口') 108 | current_path = os.path.dirname(__file__) 109 | 110 | 111 | class BaseHandler(RequestHandler): 112 | def set_default_headers(self): 113 | self.set_header("Access-Control-Allow-Origin", "*") # 这个地方可以写域名 114 | self.set_header("Access-Control-Allow-Headers", "x-requested-with") 115 | self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') 116 | 117 | def post(self): 118 | self.write('some post') 119 | 120 | def get(self): 121 | self.write('some get') 122 | 123 | def options(self): 124 | self.set_status(204) 125 | self.finish() 126 | 127 | 128 | class BasicHandler(BaseHandler): 129 | """股票基本信息""" 130 | def get(self): 131 | ts_code = self.get_argument('ts_code') 132 | basic = get_stock_basic(ts_code) 133 | basic['symbol'] = basic['ts_code'] 134 | results = {"msg": "success", "basic": [basic]} 135 | self.write(json.dumps(results, ensure_ascii=False)) 136 | 137 | 138 | class KlineHandler(BaseHandler): 139 | """K 线""" 140 | def get(self): 141 | ts_code = self.get_argument('ts_code') 142 | freq = self.get_argument('freq') 143 | asset = self.get_argument('asset', "E") 144 | trade_date = self.get_argument('trade_date') 145 | if trade_date == 'null': 146 | trade_date = datetime.now().date().__str__().replace("-", "") 147 | kline = get_kline(ts_code=ts_code, end_date=trade_date, freq=freq, asset=asset) 148 | kline.loc[:, "dt"] = pd.to_datetime(kline.dt) 149 | 150 | ka = KlineAnalyze(kline, bi_mode="new", verbose=False, use_xd=True, max_count=5000) 151 | kline = ka.to_df(ma_params=(5, 20), use_macd=True, max_count=5000, mode='new') 152 | kline = kline.fillna("") 153 | kline.loc[:, "dt"] = kline.dt.apply(str) 154 | columns = ["dt", "open", "close", "low", "high", "vol", 'fx_mark', 'fx', 'bi', 'xd'] 155 | 156 | self.finish({'kdata': kline[columns].values.tolist()}) 157 | 158 | 159 | if __name__ == '__main__': 160 | parse_command_line() 161 | app = Application([ 162 | ('/kline', KlineHandler), 163 | ('/basic', BasicHandler), 164 | (r'^/(.*?)$', StaticFileHandler, {"path": os.path.join(current_path, "web"), 165 | "default_filename": "index.html"}), 166 | ], 167 | static_path=os.path.join(current_path, "web"), 168 | dubug=True 169 | ) 170 | http_server = HTTPServer(app) 171 | http_server.listen(options.port) 172 | IOLoop.current().start() 173 | 174 | 175 | # http://localhost:8005/?ts_code=000001.SH&asset=I&trade_date=20200613&freqs=D,30min,5min,1min 176 | 177 | -------------------------------------------------------------------------------- /web/fonts/element-icons.535877f5.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengbin93/czsc_web_ui/686ec6daefc9ef43dcee7c99433f79631c0f84c5/web/fonts/element-icons.535877f5.woff -------------------------------------------------------------------------------- /web/fonts/element-icons.732389de.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengbin93/czsc_web_ui/686ec6daefc9ef43dcee7c99433f79631c0f84c5/web/fonts/element-icons.732389de.ttf -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 |