├── .gitignore ├── README.md ├── app.py ├── config.py ├── requirements.txt ├── static ├── dashboard.css ├── signin.css └── starter-template.css └── templates ├── assets.html ├── dashboard.html ├── exchanges.html ├── hub.html ├── index.html ├── login.html ├── nav.html ├── signup.html └── strategy.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | _obj 3 | .tmp 4 | _test 5 | __pycache__ 6 | *.sqlite 7 | *.sqlite3 8 | *.pyc 9 | *.sh 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 零成本快速打造你自己专属的多用户量化交易平台 2 | 3 | > 本范例项目展示了使用简单的HTML页面、python服务端程序 构建一个功能强大的量化交易平台。 4 | 5 | 长期以来,量化交易平台 因其涉及技术种类多(前端,后台,数据库,回测系统,网络访问 等等),跨学科(金融、数学、计算机编程等),项目设计周期长,维护成本高 等诸多因素。造成 一些有志于 在程序化交易 、量化交易 上大展身手的 投资、资产管理团队,交易工作室,宽客爱好者等 中小交易者 望而却步! 6 | 7 | 术业专攻一直是FMZ(发明者量化) 秉承的 发展理念,如今是信息、技术 飞速发展的时代。速度几乎决定着 一个项目的成败,一次投资的成败。只有更高的效率才是制胜的根本。 8 | 9 | FMZ 对于 技术底层做出了强有力的支持,只需使用 FMZ 的 扩展 API 接口,就可以把你从繁杂的计算机技术、各个学科专业知识等问题中解放出来。 10 | 11 | 仅仅只需要开发一个 WEB站点 、APP 或者 微信小程序 对接 FMZ 的技术底层 ,就可以实现一个专业的量化交易平台。 12 | 13 | - ### 嵌入现有系统 14 | 15 | 根据本DEMO项目可以参考编写服务端代码,增加前端页面以用来嵌入现有论坛,博客,社区等系统。 16 | 以实现灵活接入现有用户群体,并且现有用户群体完全体验不到FMZ的底层技术支持,用户使用更加简洁,易操作。 17 | 18 | - ### 支持市场 19 | 20 | - CTP 商品期货 (上期所、郑商所、大商所、中金所) 21 | - 易盛外盘 (CME, CBOT等主流国外期货交易所) 22 | - 全球交易30多个区块链资产交易平台 23 | 24 | - ### 打造属于自己的量化平台 25 | 26 | - 高度自由的策略设计 27 | 28 | 使用 Python 、JavaScript 、C++ 语言编写 量化交易策略,自由定制,可以在量化交易的世界天马行空般的实现自己的交易思路。 29 | 30 | - 强大高效的回测系统 31 | 32 | 从此再也不用辛苦收集数据,本地回测系统引擎 只用一个命令轻松配置,链接:https://github.com/fmzquant/backtest_python 33 | 34 | - 精简的架构 35 | 36 | 只用编写几个 前端页面,一个HTTP服务端程序,即可轻松搭建。 37 | 38 | - ### DEMO项目 39 | 40 | - 名称:FMZ演示如何使用FMZ的扩展API打造自己的资产管理量化平台 41 | 42 | - 本DEMO项目 安装 43 | 44 | - 首先 clone 本DEMO项目 45 | 46 | ``` 47 | git clone https://github.com/fmzquant/fmz_extend_api_demo.git 48 | ``` 49 | 50 | ![alt](https://www.fmz.com/upload/asset/c36383238f93ca220887b7d85e1a611ba3a99007.png) 51 | 52 | - 切换到这个 目录,执行 pip 安装 53 | 54 | ![alt](https://www.fmz.com/upload/asset/6074daa004ede3ce30eae01c0c7208a5db9708f5.png) 55 | 56 | ``` 57 | pip install -r requirements.txt 58 | ``` 59 | 60 | ![alt](https://www.fmz.com/upload/asset/c4bdf77264d876f73dd628811865f484bb0992b7.png) 61 | 62 | 注意:如果提示 Permission denied , 需要 sudo pip install -r requirements.txt 这样执行 pip ,根据要求输入操作系统密码。 63 | 64 | - 安装完成后,配置一下 服务端程序 要使用的 FMZ 账号的 API KEY 65 | 66 | > FMZ 扩展 API KEY 使用 详见 FMZ API 文档:https://www.fmz.com/api#FMZ%20%E5%B9%B3%E5%8F%B0%E6%89%A9%E5%B1%95API 67 | 68 | 创建 FMZ API KEY 69 | 70 | ![alt](https://www.fmz.com/upload/asset/28b430e0104147594a264d838838735db4114d9b.png) 71 | 72 | 把 API KEY 写入 ,本DEMO 的 app.py 服务端程序。 73 | 74 | ![alt](https://www.fmz.com/upload/asset/426bb928998875dd0e7fbf5f43fed546a3ac2f2f.png ) 75 | 76 | - 本DEMO项目 服务端运行命令 77 | 78 | ``` 79 | python app.py 80 | ``` 81 | 82 | - 运行显示: 83 | ![alt](https://www.fmz.com/upload/asset/60bb0b2e41e31d7354a461a63300841c24658a7f.png) 84 | 运行服务端程序后,在浏览器打开本地页面:http://127.0.0.1:5000 85 | ![alt](https://www.fmz.com/upload/asset/6e179f4b1dd680dbcc4f8b96d189f289d780e853.png) 86 | 87 | - 测试注册页面 88 | 89 | ![alt](https://www.fmz.com/upload/asset/83b09142e42ae0ff4d9c8f789a771fb99c1f2d48.png) 90 | 本项目 DEMO 量化平台 已经运行起来了,注册好 这个测试平台的 账号(储存在本地数据的),登录进去 配置 作为这个平台用户的 交易所API KEY。 91 | 92 | ![alt](https://www.fmz.com/upload/asset/d38f7155af07c0231dcdf632887585042268d058.png) 93 | ![alt](https://www.fmz.com/upload/asset/2c6f6c8021a8d69e357a2e0fe538f3a919f3f8b4.png) 94 | 95 | 现在配置好了如图: 96 | 97 | ![alt](https://www.fmz.com/upload/asset/d7206a4113e2974683a614f455be8dc4fbce9f43.png) 98 | 99 | 页面显示的三个策略 仅仅是 UI显示,这些还需要 资产管理量化平台 的管理者 具体设计实现,这里只做演示用。 100 | 101 | - 配置一个测试策略 102 | 103 | 本DEMO项目 ,服务端 会检测到 “一键启动” 按钮按下,触发搜索FMZ账号中 包含 "main" 关键字的策略,使用该策略 绑定机器人运行。 104 | 所以我们先创建一个 名为 main Test profit 的策略 105 | 106 | main Test profit 策略代码如下: 107 | 108 | ```javascript 109 | function main() { 110 | while(true) { 111 | LogProfit(Math.random()*100); 112 | Sleep(1000); 113 | } 114 | } 115 | ``` 116 | 117 | ![alt](https://www.fmz.com/upload/asset/52792c59a5db460c0bdf5a229803b92f92b8cb07.png) 118 | 119 | 编辑代码后,点击保存。 120 | 注意:在运行前必须确保有一个托管者在线,认识托管者:https://www.fmz.com/bbs-topic/463 。 121 | 122 | - 点击 “一键启动” 按钮, 会自动创建一个 机器人 运行,这个机器人 只会 随机输出数值作为收益数值显示出来。 123 | 124 | 可以看到 在FMZ的控制中心上显示 出一个 新创建的机器人: 125 | ![alt](https://www.fmz.com/upload/asset/61ff0f2319aaeb4138e43de626b2a0cf6b357435.png) 126 | 127 | DEMO 网页上也显示出对应的 随机数值 128 | ![alt](https://www.fmz.com/upload/asset/73bb8cde3237d39e927edcaf3cf7a6187d174c1d.png) 129 | 130 | - 在FMZ 上运行的机器人 由 appId 识别 当前DEMO平台 登录的 用户 131 | 132 | ![alt](https://www.fmz.com/upload/asset/0d9a9751442b9dc78ba2a0c3b3bc2347c5cd8ab9.png) 133 | 134 | ```python 135 | def robot_run(robotId, appId, exchanges): 136 | strategyId = -1 137 | # 从策略库里选出一个包含main字符串的策略运行, 也可以预定义 138 | for ele in api("GetStrategyList")['data']['result']['strategies']: 139 | if 'main' in ele['name']: 140 | strategyId = ele['id'] 141 | if strategyId < 0: 142 | raise u"not found strategy" 143 | settings = { 144 | "name":"robot for %s" % (appId, ), 145 | "args": [], # our custom arguments for this strategey 146 | "appid": appId, # 为该机器人设置标签,关联到本用户 147 | "period": 60, 148 | "strategy": strategyId, 149 | "exchanges": [], 150 | } 151 | for e in exchanges: 152 | settings["exchanges"].append({"eid": e.eid, "pair": get_default_stock(e.eid), "meta" :{"AccessKey": e.accessKey, "SecretKey": e.secretKey}}) 153 | if robotId > 0: 154 | return api('RestartRobot', robotId, settings) 155 | else: 156 | return api('NewRobot', settings) 157 | ``` 158 | 可以看到 代码中 settings 是创建 机器人的配置信息, appid 就是用来 标记用户的。 159 | 160 | - 一个简单的交易中心 161 | 162 | DEMO附带了一个简单的交易中心, 以帮助用户了解FMZ平台扩展API 163 | 164 | ![alt](https://www.fmz.com/upload/asset/05da54385c1518514b1de45c0a484a5015a895c3.png) 165 | 166 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!encoding=utf8 2 | 3 | 4 | import datetime 5 | import json 6 | import logging 7 | import os 8 | import time 9 | 10 | try: 11 | import md5 12 | import urllib2 13 | from urllib import urlencode 14 | except Exception as e: 15 | import hashlib as md5 16 | import urllib.request as urllib2 17 | from urllib.parse import urlencode 18 | from flask import jsonify, request, Flask, render_template, redirect, url_for 19 | from flask_bootstrap import Bootstrap 20 | from flask_wtf import FlaskForm 21 | from wtforms import StringField, PasswordField 22 | from wtforms.validators import InputRequired, Email, Length 23 | from flask_sqlalchemy import SQLAlchemy 24 | from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user 25 | 26 | from config import BOTVS_ACCESS_KEY, BOTVS_SECRET_KEY 27 | 28 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [line:%(lineno)d] %(levelname)s %(message)s', 29 | datefmt='%Y-%m-%d %H:%M:%S') 30 | 31 | 32 | class cached(object): 33 | def __init__(self, *args, **kwargs): 34 | self.kv = {} 35 | self.timeout = kwargs.get("timeout", 0) 36 | 37 | def __call__(self, func): 38 | def inner(*args, **kwargs): 39 | if not kwargs.get('cache', False): 40 | return func(*args, **kwargs) 41 | k = str(func) + '|' + ','.join(args) 42 | now = time.time() 43 | if self.timeout > 0 and (k not in self.kv or (now - self.kv[k]['fetch_time'] > self.timeout)): 44 | res = func(*args, **kwargs) 45 | self.kv[k] = {'data': res, 'fetch_time': now} 46 | return self.kv[k]['data'] 47 | 48 | return inner 49 | 50 | 51 | @cached(timeout=600) 52 | def api(method, *args): 53 | d = { 54 | 'version': '1.0', 55 | 'access_key': BOTVS_ACCESS_KEY, 56 | 'method': method, 57 | 'args': json.dumps(list(args)), 58 | 'nonce': int(time.time() * 1000), 59 | } 60 | d['sign'] = md5.md5( 61 | ('%s|%s|%s|%d|%s' % (d['version'], d['method'], d['args'], d['nonce'], BOTVS_SECRET_KEY)).encode( 62 | 'utf-8')).hexdigest() 63 | return json.loads( 64 | urllib2.urlopen('https://www.fmz.com/api/v1', urlencode(d).encode('utf-8')).read().decode('utf-8')) 65 | 66 | 67 | exchanges_list = None 68 | 69 | 70 | def get_exchange_list(force=False): 71 | global exchanges_list 72 | if exchanges_list is None or force: 73 | exchanges_list = json.loads(urllib2.urlopen('https://www.fmz.com/chart/symbols.json').read()) 74 | logging.debug(' * Initialize %d exchanges' % (len(exchanges_list),)) 75 | return exchanges_list 76 | 77 | 78 | def get_default_stock(eid): 79 | for e in get_exchange_list(): 80 | if e['eid'] == eid: 81 | return e['stocks'].split(',')[0] 82 | 83 | 84 | def plugin_run(exchanges, code, pair=None, period=900): 85 | settings = {"period": period / 60, "source": code, "exchanges": []} 86 | for e in exchanges: 87 | if pair is None: 88 | pair = get_default_stock(e.eid) 89 | settings["exchanges"].append( 90 | {"eid": e.eid, "pair": pair, "meta": {"AccessKey": e.accessKey, "SecretKey": e.secretKey}}) 91 | return api('PluginRun', settings) 92 | 93 | 94 | def robot_run(robotId, appId, exchanges): 95 | strategyId = -1 96 | # 从策略库里选出一个包含main字符串的策略运行, 也可以预定义 97 | for ele in api("GetStrategyList")['data']['result']['strategies']: 98 | if 'main' in ele['name']: 99 | strategyId = ele['id'] 100 | if strategyId < 0: 101 | raise Exception(u"not found strategy") 102 | settings = { 103 | "name": "robot for %s" % (appId,), 104 | "args": [], # our custom arguments for this strategey 105 | "appid": appId, # 为该机器人设置标签,关联到本用户 106 | "period": 60, 107 | "strategy": strategyId, 108 | "exchanges": [], 109 | } 110 | for e in exchanges: 111 | settings["exchanges"].append({"eid": e.eid, "pair": get_default_stock(e.eid), 112 | "meta": {"AccessKey": e.accessKey, "SecretKey": e.secretKey}}) 113 | if robotId > 0: 114 | return api('RestartRobot', robotId, settings) 115 | else: 116 | return api('NewRobot', settings) 117 | 118 | 119 | app = Flask(__name__) 120 | app.config['SECRET_KEY'] = 'supposedtobeveryimportantsecret!' 121 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(os.path.dirname(os.path.abspath(__file__)), 122 | 'user.sqlite3') 123 | app.config['BOOTSTRAP_SERVE_LOCAL'] = True 124 | app.config['TEMPLATES_AUTO_RELOAD'] = True 125 | bootstrap = Bootstrap(app) 126 | 127 | login_manager = LoginManager() 128 | login_manager.init_app(app) 129 | login_manager.login_view = 'login' 130 | 131 | db = SQLAlchemy(app) 132 | 133 | 134 | class User(UserMixin, db.Model): 135 | id = db.Column(db.Integer, primary_key=True) 136 | username = db.Column(db.String(15), unique=True) 137 | email = db.Column(db.String(50), unique=True) 138 | password = db.Column(db.String(80)) 139 | date = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now) 140 | 141 | 142 | class Exchange(db.Model): 143 | id = db.Column(db.Integer, primary_key=True) 144 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 145 | eid = db.Column(db.String(80)) 146 | label = db.Column(db.String(80)) 147 | accessKey = db.Column(db.Text) 148 | secretKey = db.Column(db.Text) 149 | date = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now) 150 | 151 | 152 | db.create_all() 153 | db.session.commit() 154 | 155 | 156 | @login_manager.user_loader 157 | def load_user(user_id): 158 | return User.query.get(int(user_id)) 159 | 160 | 161 | class LoginForm(FlaskForm): 162 | username = StringField(u'用户名', validators=[InputRequired(), Length(min=4, max=15)]) 163 | password = PasswordField(u'密码', validators=[InputRequired(), Length(min=8, max=80)]) 164 | 165 | 166 | class RegisterForm(FlaskForm): 167 | email = StringField(u'邮箱', validators=[InputRequired(), Email(message='无效的Email'), Length(max=50)]) 168 | username = StringField(u'用户名', validators=[InputRequired(), Length(min=4, max=15)]) 169 | password = PasswordField(u'密码', validators=[InputRequired(), Length(min=8, max=80)]) 170 | 171 | 172 | @app.route('/') 173 | def index(): 174 | return render_template('index.html', current_user=current_user) 175 | 176 | 177 | @app.route('/update', methods=['GET', 'POST']) 178 | def update(): 179 | return jsonify(get_exchange_list(True)) 180 | 181 | 182 | @app.route('/login', methods=['GET', 'POST']) 183 | def login(): 184 | form = LoginForm() 185 | error = None 186 | if current_user.is_authenticated: 187 | return redirect(url_for('dashboard')) 188 | 189 | if form.validate_on_submit(): 190 | user = User.query.filter_by(username=form.username.data).first() 191 | if user: 192 | if user.password == md5.md5((user.email + '__slat__' + form.password.data).encode("utf8")).hexdigest(): 193 | login_user(user) 194 | return redirect(url_for('dashboard')) 195 | error = u'用户名或密码错误' 196 | return render_template('login.html', current_user=current_user, form=form, error=error) 197 | 198 | 199 | @app.route('/signup', methods=['GET', 'POST']) 200 | def signup(): 201 | form = RegisterForm() 202 | if form.validate_on_submit(): 203 | hashed_password = md5.md5((form.email.data + '__slat__' + form.password.data).encode("utf8")).hexdigest() 204 | ele = User(username=form.username.data, email=form.email.data, password=hashed_password) 205 | db.session.add(ele) 206 | db.session.commit() 207 | login_user(ele) 208 | return redirect(url_for('dashboard')) 209 | 210 | return render_template('signup.html', current_user=current_user, form=form) 211 | 212 | 213 | @app.route('/dashboard') 214 | @login_required 215 | def dashboard(): 216 | appId = "appId_%d" % (current_user.id,) 217 | # 通过标签或者对应的该用户所有的运行的机器人 218 | robots = api('GetRobotList', appId)['data']['result']['robots'] 219 | robotId = -1 220 | profit = .0 221 | if len(robots) > 0: 222 | robotId = robots[0]['id'] 223 | isRunning = False 224 | for ele in robots: 225 | profit = ele['profit'] 226 | if ele['status'] < 3: 227 | isRunning = True 228 | break 229 | if request.method == "GET": 230 | result = None 231 | action = request.args.get('action', None) 232 | if action: 233 | if action == "refresh": 234 | # 刷新收益 235 | return jsonify({'profit': profit, 'running': isRunning}) 236 | elif action == "run": 237 | # 运行策略 238 | result = robot_run(robotId, appId, Exchange.query.filter_by(user_id=current_user.id).all()) 239 | elif action == "stop": 240 | # 停止策略(停止该用户所有的策略) 241 | for ele in robots: 242 | result = api('StopRobot', ele['id']) 243 | if not result: 244 | result = {'code': 0} 245 | return jsonify(result) 246 | 247 | platforms = Exchange.query.filter_by(user_id=current_user.id).all() 248 | return render_template('dashboard.html', current_user=current_user, platforms=platforms, running=isRunning, 249 | profit=profit) 250 | 251 | 252 | @app.route('/exchanges', methods=['GET', 'POST']) 253 | @login_required 254 | def exchanges(): 255 | error = None 256 | if request.method == 'POST': 257 | eid = request.form.get('eid', None) 258 | label = request.form.get('label', None) 259 | accessKey = request.form.get('accessKey', None) 260 | secretKey = request.form.get('secretKey', None) 261 | if eid and label and accessKey and secretKey: 262 | ele = Exchange(user_id=current_user.id, eid=eid, label=label, accessKey=accessKey, secretKey=secretKey) 263 | db.session.add(ele) 264 | db.session.commit() 265 | return redirect(url_for('assets')) 266 | error = u"表格填写不完整" 267 | return render_template('dashboard.html', current_user=current_user, exchanges=get_exchange_list(), error=error) 268 | 269 | 270 | @app.route('/assets', methods=['GET', 'POST']) 271 | @login_required 272 | def assets(): 273 | if request.method == "GET": 274 | action = request.args.get('action', None) 275 | if action == "del": 276 | db.session.delete(Exchange.query.filter_by(user_id=current_user.id, id=request.args.get('pid', -1)).first()) 277 | db.session.commit() 278 | return jsonify(results=True) 279 | platforms = Exchange.query.filter_by(user_id=current_user.id).all() 280 | return render_template('dashboard.html', current_user=current_user, platforms=platforms) 281 | 282 | 283 | @app.route('/hub', methods=['GET', 'POST']) 284 | @login_required 285 | def hub(): 286 | if request.method == "GET": 287 | action = request.args.get('action', None) 288 | symbol = request.args.get('symbol', None) 289 | if action is not None: 290 | action = action.lower().strip() 291 | if symbol is not None: 292 | symbol = symbol.split('.')[1] 293 | args = json.loads(request.args.get('args', '[]')) 294 | if action == "market": 295 | # 手动刷新市场行情 296 | r = plugin_run([Exchange.query.filter_by(user_id=current_user.id, id=request.args.get('pid', -1)).first()], ''' 297 | function main() { 298 | exchange.SetTimeout(2000); 299 | var a = exchange.Go("GetTicker"); 300 | var b = exchange.Go("GetDepth") 301 | var c = exchange.Go("GetRecords"); 302 | return [a.wait(),b.wait(),c.wait()]; 303 | } 304 | ''', symbol, args[0]) 305 | return jsonify(r) 306 | elif action == "buy" or action == "sell": 307 | r = plugin_run([Exchange.query.filter_by(user_id=current_user.id, id=request.args.get('pid', -1)).first()], ''' 308 | function main() { 309 | exchange.SetTimeout(2000); 310 | var pfn = '%s' == 'buy' ? exchange.Buy : exchange.Sell; 311 | return pfn(%f, %f); 312 | } 313 | ''' % (action, args[0], args[1]), symbol) 314 | return jsonify(r) 315 | elif action == "cancel": 316 | r = plugin_run([Exchange.query.filter_by(user_id=current_user.id, id=request.args.get('pid', -1)).first()], ''' 317 | function main() { 318 | exchange.SetTimeout(2000); 319 | return exchange.CancelOrder(%s) 320 | } 321 | ''' % (args[0]), symbol) 322 | return jsonify(r) 323 | elif action == "balance": 324 | # 运行查询脚本(也可以定制实现任何想要的功能, 比如资产统计,一键平仓) 325 | r = plugin_run([Exchange.query.filter_by(user_id=current_user.id, id=request.args.get('pid', -1)).first()], ''' 326 | function main() { 327 | exchange.SetTimeout(2000); 328 | return [exchange.GetOrders(), exchange.GetAccount()]; 329 | } 330 | ''', symbol) 331 | return jsonify(r) 332 | platforms = Exchange.query.filter_by(user_id=current_user.id).all() 333 | arr = [] 334 | es = get_exchange_list() 335 | for ele in platforms: 336 | for obj in es: 337 | if obj['eid'] == ele.eid: 338 | arr.append({'id': ele.id, 'pid': ele.id, 'name': obj['name'], 'symbols': obj['symbols'], 'eid': ele.eid, 339 | 'label': ele.label}) 340 | break 341 | return render_template('dashboard.html', current_user=current_user, platforms=json.dumps(arr)) 342 | 343 | 344 | @app.route('/logout') 345 | @login_required 346 | def logout(): 347 | logout_user() 348 | return redirect(url_for('index')) 349 | 350 | 351 | if __name__ == '__main__': 352 | app.run(debug=False, threaded=True, host='127.0.0.1', port=5000) 353 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | import os 5 | 6 | # 配置BotVS平台的AccessKey与SecretKey (修改xxxxx/yyyyy) 7 | BOTVS_ACCESS_KEY = os.getenv('BOTVS_ACCESS_KEY', 'xxxxx') 8 | BOTVS_SECRET_KEY = os.getenv('BOTVS_SECRET_KEY', 'yyyyy') 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0.0 2 | Flask-Bootstrap==3.3.7.1 3 | Flask-Login==0.4.1 4 | Flask-SQLAlchemy==2.3.2 5 | Flask-WTF==0.14.2 6 | -------------------------------------------------------------------------------- /static/dashboard.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Base structure 3 | */ 4 | 5 | /* Move down content because we have a fixed navbar that is 50px tall */ 6 | body { 7 | padding-top: 50px; 8 | } 9 | 10 | 11 | /* 12 | * Global add-ons 13 | */ 14 | 15 | .sub-header { 16 | padding-bottom: 10px; 17 | border-bottom: 1px solid #eee; 18 | } 19 | 20 | /* 21 | * Top navigation 22 | * Hide default border to remove 1px line. 23 | */ 24 | .navbar-fixed-top { 25 | border: 0; 26 | } 27 | 28 | /* 29 | * Sidebar 30 | */ 31 | 32 | /* Hide for mobile, show later */ 33 | .sidebar { 34 | display: none; 35 | } 36 | @media (min-width: 768px) { 37 | .sidebar { 38 | position: fixed; 39 | top: 51px; 40 | bottom: 0; 41 | left: 0; 42 | z-index: 1000; 43 | display: block; 44 | padding: 20px; 45 | overflow-x: hidden; 46 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ 47 | background-color: #f5f5f5; 48 | border-right: 1px solid #eee; 49 | } 50 | } 51 | 52 | /* Sidebar navigation */ 53 | .nav-sidebar { 54 | margin-right: -21px; /* 20px padding + 1px border */ 55 | margin-bottom: 20px; 56 | margin-left: -20px; 57 | } 58 | .nav-sidebar > li > a { 59 | padding-right: 20px; 60 | padding-left: 20px; 61 | } 62 | .nav-sidebar > .active > a, 63 | .nav-sidebar > .active > a:hover, 64 | .nav-sidebar > .active > a:focus { 65 | color: #fff; 66 | background-color: #428bca; 67 | } 68 | 69 | 70 | /* 71 | * Main content 72 | */ 73 | 74 | .main { 75 | padding: 20px; 76 | } 77 | @media (min-width: 768px) { 78 | .main { 79 | padding-right: 40px; 80 | padding-left: 40px; 81 | } 82 | } 83 | .main .page-header { 84 | margin-top: 0; 85 | } 86 | 87 | 88 | /* 89 | * Placeholder dashboard ideas 90 | */ 91 | 92 | .placeholders { 93 | margin-bottom: 30px; 94 | text-align: center; 95 | } 96 | .placeholders h4 { 97 | margin-bottom: 0; 98 | } 99 | .placeholder { 100 | margin-bottom: 20px; 101 | } 102 | .placeholder img { 103 | display: inline-block; 104 | border-radius: 50%; 105 | } 106 | -------------------------------------------------------------------------------- /static/signin.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 40px; 3 | padding-bottom: 40px; 4 | background-color: #eee; 5 | } 6 | 7 | .form-signin { 8 | max-width: 330px; 9 | padding: 15px; 10 | margin: 0 auto; 11 | } 12 | .form-signin .form-signin-heading, 13 | .form-signin .checkbox { 14 | margin-bottom: 10px; 15 | } 16 | .form-signin .checkbox { 17 | font-weight: normal; 18 | } 19 | .form-signin .form-control { 20 | position: relative; 21 | height: auto; 22 | -webkit-box-sizing: border-box; 23 | -moz-box-sizing: border-box; 24 | box-sizing: border-box; 25 | padding: 10px; 26 | font-size: 16px; 27 | } 28 | .form-signin .form-control:focus { 29 | z-index: 2; 30 | } 31 | .form-signin input[type="email"] { 32 | margin-bottom: -1px; 33 | border-bottom-right-radius: 0; 34 | border-bottom-left-radius: 0; 35 | } 36 | .form-signin input[type="password"] { 37 | margin-bottom: 10px; 38 | border-top-left-radius: 0; 39 | border-top-right-radius: 0; 40 | } 41 | -------------------------------------------------------------------------------- /static/starter-template.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | .starter-template { 5 | padding: 40px 15px; 6 | text-align: center; 7 | } 8 | -------------------------------------------------------------------------------- /templates/assets.html: -------------------------------------------------------------------------------- 1 | {% if platforms %} 2 | 37 |
38 | 39 |
已绑定资产
40 |
41 |

运行策略时可以跟交易所绑定起来 添加资产

42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {% for ele in platforms %} 57 | 58 | 59 | 60 | 61 | 62 | 66 | 67 | {% endfor %} 68 | 69 |
#交易所标签添加时间操作
{{ele.id}}{{ ele.eid }}{{ ele.label }}{{ ele.date }} 63 | 删除 64 | 交易 65 |
70 |
71 | {% else %} 72 | 75 | {% endif %} 76 | -------------------------------------------------------------------------------- /templates/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | 3 | {% block title %} 4 | 控制中心 5 | {% endblock %} 6 | 7 | {% block styles %} 8 | {{super()}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% if request.path == '/hub' %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% endif %} 25 | 26 | {% endblock %} 27 | {% block content %} 28 | {% include "nav.html" %} 29 |
30 |
31 | 39 |
40 | {% if request.path == '/dashboard' %} 41 | {% include "strategy.html" %} 42 | {% else %} 43 | {% include request.path[1:]+".html" %} 44 | {% endif %} 45 |
46 |
47 |
48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /templates/exchanges.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
绑定交易所
4 |
5 |
6 |
7 | 8 |
9 | 14 |
15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 | {% if error %} 43 | 46 | {% endif %} 47 | 48 | -------------------------------------------------------------------------------- /templates/hub.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | {% raw %} 6 |
7 | 10 |
11 |
12 |
13 | Loading... 14 |
15 |
16 | High: {{ticker.high}} 17 | Low: {{ticker.low}} 18 | Bid: {{ticker.bid}} 19 | Ask: {{ticker.ask}} 20 | Last: {{ticker.last}} 21 | Vol: {{ticker.vol}} 22 |
23 |
24 |
25 |
26 | 27 |
28 | 29 | 30 |
31 |
32 | Update: {{update}} 33 | 34 | 35 |
36 |
37 |
38 | 52 |
53 |
54 |
55 |
Empty
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 75 | 76 |
IdPriceAmount
{{ order.Id }}{{ ['Bid', 'Ask'][order.Type]}}{{ order.Price }}{{ order.Amount}} 73 | 74 |
77 |
78 |
79 |
80 |
Empty
81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
TimePriceAmountFeeState
{{ order.created }}{{ ['Limit', 'Market'][order.type] }} {{ ['Bid', 'Ask'][order.side] }}({{ order.avg_price}}){{ order.price }}({{ order.deal_amount}}){{ order.amount}}{{ order.fee}}{{ order.state == 1 ? 'Filled' : 'Cancelled'}}
102 | 110 |
111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
AssetsFreeFrozen
{{ item.currency }}{{ item.free.toFixed(8) }}{{ item.frozen.toFixed(8) }}
126 |
127 |
128 |
129 |
130 |
131 |
132 |
Last: {{ticker.last || '--'}} Sync
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 |
#AmountBidAskAmount
{{$index+1}} {{ row[1][1] }} {{ row[1][0] }} {{ row[0][0] }} {{ row[0][1] }}
153 |
154 |
155 |
Trade Market
156 |
157 |
158 |
159 | 160 |
161 |
162 | 163 |
164 |
165 |
166 |
167 | 168 |
169 |
170 | 171 |
172 |
173 |
174 |
175 | 176 |
177 |
178 | 179 |
180 |
181 |
182 |
183 | {{ assets.length == 2 ? assets[0].free.toFixed(8) : ''}} 184 |
185 |
186 | {{ assets.length == 2 ? assets[1].free.toFixed(8) : '' }} 187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | 196 | 716 | {% endraw %} 717 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | 3 | {% block title %} 4 | BotVS API Demo 5 | {% endblock %} 6 | 7 | {% block styles %} 8 | {{super()}} 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 | {% include "nav.html" %} 14 |
15 |
16 |

BotVS

17 |

演示如何使用FMZ的扩展API打造自己的资产管理量化平台

18 |

连接国内商品期货, 外盘CME/CBOT等, 以及主流全球35家区块链资产交易所, 策略运行调度全部托管于发明者量化(FMZ.COM)

19 | 详细API文档 20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %} 5 | 登录 6 | {% endblock %} 7 | 8 | {% block styles %} 9 | {{super()}} 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 | {% include "nav.html" %} 15 |
16 |
17 | {% if error %} 18 | 21 | {% endif %} 22 | 23 | {{ form.hidden_tag() }} 24 | {{ wtf.form_field(form.username) }} 25 | {{ wtf.form_field(form.password) }} 26 | 27 |
28 | 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/nav.html: -------------------------------------------------------------------------------- 1 | {% if not current_user.is_authenticated %} 2 | 24 | {% else %} 25 | 44 | {% endif %} 45 | -------------------------------------------------------------------------------- /templates/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %} 5 | Sign Up 6 | {% endblock %} 7 | 8 | {% block styles %} 9 | {{super()}} 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 |
15 | {% include "nav.html" %} 16 |
17 | 18 | {{ form.hidden_tag() }} 19 | {{ wtf.form_field(form.username) }} 20 | {{ wtf.form_field(form.email) }} 21 | {{ wtf.form_field(form.password) }} 22 | 23 |
24 | 25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/strategy.html: -------------------------------------------------------------------------------- 1 | {% if platforms %} 2 | 51 |
52 | 53 |
策略组合
54 |
55 | 可以自定义资金分配比例 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 73 | 76 | 81 | 82 | 83 | 84 | 91 | 94 | 99 | 100 | 101 | 102 | 109 | 112 | 117 | 118 |
策略名称资金占比风格交易平台
追涨杀跌趋势策略 67 |
68 |
69 | 50% 70 |
71 |
72 |
74 | 长线求稳 75 | 77 | {% for ele in platforms %} 78 | {{ ele.label }} 79 | {% endfor %} 80 |
对冲高频套利交易 85 |
86 |
87 | 30% 88 |
89 |
90 |
92 | 微利 93 | 95 | {% for ele in platforms %} 96 | {{ ele.label }} 97 | {% endfor %} 98 |
区块链资产轮动策略 103 |
104 |
105 | 20% 106 |
107 |
108 |
110 | 只交易优质链 111 | 113 | {% for ele in platforms %} 114 | {{ ele.label }} 115 | {% endfor %} 116 |
119 |
120 |
121 |
122 | 当前利润: {{ profit }} 123 |
124 |
125 |
126 | 127 | 128 |
129 |
130 |
131 | {% else %} 132 | 135 | {% endif %} 136 | --------------------------------------------------------------------------------