├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── app ├── app.py ├── check_preds.py ├── performance.py ├── predict.py ├── run_charts.py ├── run_charts_extended.py ├── run_charts_performance.py ├── static │ ├── css │ │ ├── bootstrap.css │ │ ├── bootstrap.min.css │ │ └── portfolio-item.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ └── jquery.js └── templates │ ├── extended.html │ ├── index.html │ ├── layout.html │ └── performance.html ├── collect-data ├── collect_books.py └── collect_trades.py ├── images ├── live_results.png ├── strategy01.png ├── strategy05.png └── strategy10.png ├── model ├── __init__.py ├── features.py ├── model.py └── strategy.py ├── mongo-queries └── group_ltc.js └── presentation.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *data/* 3 | *.pkl 4 | *.swp 5 | *ipynb* 6 | *0.0.0.0* 7 | *.sh 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitpredict 2 | 3 | ## Summary 4 | This project aims to make high frequency bitcoin price predictions from market microstructure data. The dataset is a series of one second snapshots of open buy and sell orders on the Bitfinex exchange, combined with a record of executed transactions. Data collection began 08/20/2015. 5 | 6 | A number of engineered features are used to train a Gradient Boosting model, and a theoretical trading strategy is simulated on historical and live data. 7 | 8 | ## Target 9 | The target for prediction is the midpoint price 30 seconds in the future. The midpoint price is the average of the best bid price and the best ask price. 10 | 11 | ## Features 12 | 13 | #### Width 14 | This is the difference between the best bid price and best ask price. 15 | 16 | #### Power Imbalance 17 | This is a measure of imbalance between buy and sell orders. For each order, a weight is calculated as the inverse distance to the current midpoint price, raised to a power. Total weighted sell order volume is then subtracted from total weighted buy order volume. Powers of 2, 4, and 8 are used to create three separate features. 18 | 19 | #### Power Adjusted Price 20 | This is similar to Power Imbalance, but the weighted distance to the current midpoint price (not inverted) is used for a weighted average of prices. The percent change from the current midpoint price to the weighted average is then calculated. Powers of 2, 4, and 8 are used to create three separate features. 21 | 22 | #### Trade Count 23 | This is the number of trades in the previous X seconds. Offsets of 30, 60, 120, and 180 are used to create four separate features. 24 | 25 | #### Trade Average 26 | This is the percent change from the current midpoint price to the average of trade prices in the previous X seconds. Offsets of 30, 60, 120, and 180 are used to create four separate features. 27 | 28 | #### Aggressor 29 | This is measure of whether buyers or sellers were more aggressive in the previous X seconds. A buy aggressor is calculated as a trade where the buy order was more recent than the sell order. A sell aggressor is the reverse. The total volume created by sell aggressors is subtracted from the total volume created by buy aggressors. Offsets of 30, 60, 120, and 180 are used to create four separate features. 30 | 31 | #### Trend 32 | This is the linear trend in trade prices over the previous X seconds. Offsets of 30, 60, 120, and 180 are used to create four separate features. 33 | 34 | ## Model 35 | The above features are used to train a Gradient Boosting model. The model is validated using a shifting 100,000 second window where test data always occurs after training data. The length of training data accumulates with each successive iteration. Average out of sample R-squared is used as an evaluation metric. With four weeks of data, an out of sample R-squared of 0.0846 is achieved. 36 | 37 | ## Backtest Results 38 | A theoretical trading strategy is implemented to visualize model performance. At any model prediction above a threshold, a simulated position is initiated and held for 30 seconds, with only one position allowed at a time. Theoretical execution is done at the midpoint price without transaction costs. 39 | 40 | The results at different thresholds can be seen below. Three weeks of data are used for training, with one week of data used for theoretical trading. 41 | 42 | ![Strategy with a 0.01% trading threshold.](images/strategy01.png) 43 | 44 | ![Strategy with a 0.05% trading threshold.](images/strategy05.png) 45 | 46 | ## Live Results 47 | The model was run on live data and theoretical results were displayed on a web app. Performance with a 0.01% trading threshold can be seen below. 48 | 49 | ![Live strategy with a 0.01% trading threshold.](images/live_results.png) 50 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/__init__.py -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | app = Flask(__name__) 3 | 4 | 5 | @app.route('/') 6 | def index(): 7 | return render_template('index.html') 8 | 9 | 10 | @app.route('/extended') 11 | def extended(): 12 | return render_template('extended.html') 13 | 14 | 15 | @app.route('/performance') 16 | def performance(): 17 | return render_template('performance.html') 18 | 19 | if __name__ == '__main__': 20 | app.run(host='0.0.0.0', port=80) 21 | -------------------------------------------------------------------------------- /app/check_preds.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import pymongo 3 | from sklearn.metrics import r2_score 4 | from math import log 5 | import sys 6 | 7 | client = pymongo.MongoClient() 8 | db = client['bitmicro'] 9 | predictions = db['btc_predictions'] 10 | 11 | if len(sys.argv) > 1: 12 | limit = int(sys.argv[1]) 13 | cursor = predictions.find().limit(limit).sort('_id', pymongo.DESCENDING) 14 | else: 15 | cursor = predictions.find().sort('_id', pymongo.DESCENDING) 16 | 17 | df = pd.DataFrame(list(cursor)) 18 | df = df[df.future_price != 0] 19 | df['actual'] = (df.future_price/df.price).apply(log) 20 | score = r2_score(df.actual.values, df.prediction.values) 21 | print 'observations:', len(df) 22 | print 'r^2:', score 23 | -------------------------------------------------------------------------------- /app/performance.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | import time 3 | import sys 4 | import pandas as pd 5 | 6 | client = pymongo.MongoClient() 7 | db = client['bitmicro'] 8 | symbol = sys.argv[1] 9 | frequency = int(sys.argv[2]) 10 | predictions = db[symbol+'_predictions'] 11 | performance = db[symbol+'_performance'] 12 | 13 | print 'Running...' 14 | last_timestamp = time.time() - frequency 15 | while True: 16 | start = time.time() 17 | 18 | query = {'_id': {'$gt': last_timestamp}} 19 | cursor = predictions.find(query).sort('_id', pymongo.DESCENDING) 20 | data = pd.DataFrame(list(cursor)) 21 | if not data.empty: 22 | data = data.set_index('_id') 23 | data = data.sort_index(ascending=True) 24 | returns = (data.position*data.change).sum() 25 | last_timestamp = data.index[-1] 26 | # Set as mongo update so it doesn't blow up if data is not updating 27 | performance.update_one({'_id': last_timestamp}, 28 | {'$setOnInsert': {'returns': returns}}, 29 | upsert=True) 30 | 31 | time_delta = time.time()-start 32 | time.sleep(frequency-time_delta) 33 | -------------------------------------------------------------------------------- /app/predict.py: -------------------------------------------------------------------------------- 1 | from bitpredict.model import features as f 2 | import pymongo 3 | import time 4 | import sys 5 | import pickle 6 | import numpy as np 7 | from math import log 8 | 9 | client = pymongo.MongoClient() 10 | db = client['bitmicro'] 11 | symbol = sys.argv[1] 12 | duration = int(sys.argv[2]) 13 | threshold = float(sys.argv[3]) 14 | predictions = db[symbol+'_predictions'] 15 | 16 | with open('cols.pkl', 'r') as file1: 17 | cols = pickle.load(file1) 18 | with open('model.pkl', 'r') as file2: 19 | model = pickle.load(file2) 20 | 21 | print 'Running...' 22 | trade = 0 23 | position = 0 24 | trade_time = 0 25 | change = 0 26 | previous_price = None 27 | while True: 28 | start = time.time() 29 | try: 30 | data = f.make_features(symbol, 31 | 1, 32 | [duration], 33 | [30, 60, 120, 180], 34 | [2, 4, 8], 35 | True) 36 | pred = model.predict(data[cols].values)[0] 37 | except Exception as e: 38 | print e 39 | sys.exc_clear() 40 | else: 41 | if data.width.iloc[0] > 0: 42 | # If a trade happened in the previous second 43 | if trade != 0: 44 | position = trade 45 | trade = 0 46 | # If an open position has expired 47 | if position != 0 and (start - trade_time) >= duration+1: 48 | position = 0 49 | # If we can execute a new trade 50 | if position == 0 and abs(pred) >= threshold: 51 | trade_time = time.time() 52 | trade = np.sign(pred) 53 | price = data.mid.iloc[0] 54 | if price and previous_price: 55 | change = log(price/previous_price) 56 | else: 57 | change = 0 58 | entry = {'prediction': pred, 59 | 'price': price, 60 | 'change': change, 61 | 'trade': trade, 62 | 'position': position, 63 | 'future_price': 0} 64 | # Set as mongo update so it doesn't blow up if data is not updating 65 | predictions.update_one({'_id': data.index[0]}, 66 | {'$setOnInsert': entry}, 67 | upsert=True) 68 | previous_price = price 69 | # Put in future price to use for simple (non live) calculations 70 | predictions.update_many({'_id': {'$gt': start-duration-.5, 71 | '$lt': start-duration+.5}}, 72 | {'$set': {'future_price': price}}) 73 | time_delta = time.time()-start 74 | if time_delta < 1.0: 75 | time.sleep(1-time_delta) 76 | -------------------------------------------------------------------------------- /app/run_charts.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import pymongo 3 | from bokeh.plotting import cursession, figure, output_server, push 4 | from bokeh.models.formatters import DatetimeTickFormatter, PrintfTickFormatter 5 | from bokeh.io import vplot 6 | from bokeh import embed 7 | from json import load 8 | from urllib2 import urlopen 9 | import time 10 | 11 | client = pymongo.MongoClient() 12 | db = client['bitmicro'] 13 | collection = db['btc_predictions'] 14 | 15 | 16 | def get_data(): 17 | cursor = collection.find().limit(9*60).sort('_id', pymongo.DESCENDING) 18 | data = pd.DataFrame(list(cursor)) 19 | data = data.set_index('_id') 20 | data = data.sort_index(ascending=True) 21 | timestamps = pd.to_datetime(data.index, unit='s').to_series() 22 | prices = data.price 23 | predictions = data.prediction*10000 24 | returns = (data.position*data.change).cumsum()*10000 25 | return timestamps, prices, predictions, returns 26 | 27 | timestamps, prices, predictions, returns = get_data() 28 | output_server('bitpredict') 29 | 30 | background = '#f2f2f2' 31 | ylabel_standoff = 0 32 | xformatter = DatetimeTickFormatter(formats=dict(minutes=["%H:%M"])) 33 | yformatter = PrintfTickFormatter(format="%8.1f") 34 | p1 = figure(title=None, 35 | plot_width=750, 36 | plot_height=300, 37 | x_axis_type='datetime', 38 | min_border_top=10, 39 | min_border_bottom=33, 40 | background_fill=background, 41 | tools='', 42 | toolbar_location=None) 43 | p1.line(x=timestamps, 44 | y=prices, 45 | name='prices', 46 | color='#4271ae', 47 | line_width=1, 48 | legend='Bitcoin Bid/Ask Midpoint', 49 | line_cap='round', 50 | line_join='round') 51 | p1.legend.orientation = 'top_left' 52 | p1.legend.border_line_color = background 53 | p1.outline_line_color = None 54 | p1.xgrid.grid_line_color = 'white' 55 | p1.ygrid.grid_line_color = 'white' 56 | p1.axis.axis_line_color = None 57 | p1.axis.major_tick_line_color = None 58 | p1.axis.minor_tick_line_color = None 59 | p1.yaxis.axis_label = 'Price' 60 | p1.yaxis.axis_label_standoff = ylabel_standoff 61 | p1.xaxis.formatter = xformatter 62 | p1.yaxis.formatter = PrintfTickFormatter(format='%8.2f') 63 | p1.yaxis.major_label_text_font = 'courier' 64 | p1.xaxis.major_label_text_font = 'courier' 65 | 66 | p2 = figure(title=None, 67 | plot_width=750, 68 | plot_height=295, 69 | x_axis_type='datetime', 70 | min_border_top=5, 71 | min_border_bottom=33, 72 | background_fill=background, 73 | tools='', 74 | toolbar_location=None) 75 | p2.line(x=timestamps, 76 | y=predictions, 77 | name='predictions', 78 | color='#c82829', 79 | line_width=1, 80 | legend='30 Second Prediction', 81 | line_cap='round', 82 | line_join='round') 83 | p2.legend.orientation = 'top_left' 84 | p2.legend.border_line_color = background 85 | p2.outline_line_color = None 86 | p2.xgrid.grid_line_color = 'white' 87 | p2.ygrid.grid_line_color = 'white' 88 | p2.axis.axis_line_color = None 89 | p2.axis.major_tick_line_color = None 90 | p2.axis.minor_tick_line_color = None 91 | p2.yaxis.axis_label = 'Basis Points' 92 | p2.yaxis.axis_label_standoff = ylabel_standoff 93 | p2.xaxis.formatter = xformatter 94 | p2.yaxis.formatter = yformatter 95 | p2.yaxis.major_label_text_font = 'courier' 96 | p2.xaxis.major_label_text_font = 'courier' 97 | p2.x_range = p1.x_range 98 | 99 | p3 = figure(title=None, 100 | plot_width=750, 101 | plot_height=320, 102 | x_axis_type='datetime', 103 | min_border_top=5, 104 | min_border_bottom=10, 105 | background_fill=background, 106 | x_axis_label='Greenwich Mean Time', 107 | tools='', 108 | toolbar_location=None) 109 | p3.line(x=timestamps, 110 | y=returns, 111 | name='returns', 112 | color='#8959a8', 113 | line_width=1, 114 | legend='Cumulative Return', 115 | line_cap='round', 116 | line_join='round') 117 | p3.legend.orientation = 'top_left' 118 | p3.legend.border_line_color = background 119 | p3.outline_line_color = None 120 | p3.xgrid.grid_line_color = 'white' 121 | p3.ygrid.grid_line_color = 'white' 122 | p3.axis.axis_line_color = None 123 | p3.axis.major_tick_line_color = None 124 | p3.axis.minor_tick_line_color = None 125 | p3.yaxis.axis_label = 'Basis Points' 126 | p3.yaxis.axis_label_standoff = ylabel_standoff 127 | p3.xaxis.formatter = xformatter 128 | p3.yaxis.formatter = yformatter 129 | p3.xaxis.axis_label_standoff = 12 130 | p3.yaxis.major_label_text_font = 'courier' 131 | p3.xaxis.major_label_text_font = 'courier' 132 | p3.x_range = p1.x_range 133 | 134 | vp = vplot(p1, p2, p3) 135 | push() 136 | ip = load(urlopen('http://jsonip.com'))['ip'] 137 | ssn = cursession() 138 | ssn.publish() 139 | tag = embed.autoload_server(vp, ssn, public=True).replace('localhost', ip) 140 | html = """ 141 | {%% extends "layout.html" %%} 142 | {%% block bokeh %%} 143 | %s 144 | {%% endblock %%} 145 | """ % tag 146 | 147 | with open('templates/index.html', 'w+') as f: 148 | f.write(html) 149 | 150 | renderer = p1.select(dict(name='prices')) 151 | ds_prices = renderer[0].data_source 152 | renderer = p2.select(dict(name='predictions')) 153 | ds_predictions = renderer[0].data_source 154 | renderer = p3.select(dict(name='returns')) 155 | ds_returns = renderer[0].data_source 156 | 157 | while True: 158 | timestamps, prices, predictions, returns = get_data() 159 | ds_prices.data['x'] = timestamps 160 | ds_predictions.data['x'] = timestamps 161 | ds_returns.data['x'] = timestamps 162 | ds_prices.data['y'] = prices 163 | ds_predictions.data['y'] = predictions 164 | ds_returns.data['y'] = returns 165 | ssn.store_objects(ds_prices) 166 | ssn.store_objects(ds_predictions) 167 | ssn.store_objects(ds_returns) 168 | time.sleep(1) 169 | -------------------------------------------------------------------------------- /app/run_charts_extended.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import pymongo 3 | from bokeh.plotting import cursession, figure, output_server, push 4 | from bokeh.models.formatters import DatetimeTickFormatter, PrintfTickFormatter 5 | from bokeh.io import vplot 6 | from bokeh import embed 7 | from json import load 8 | from urllib2 import urlopen 9 | import time 10 | 11 | client = pymongo.MongoClient() 12 | db = client['bitmicro'] 13 | collection = db['btc_predictions'] 14 | 15 | 16 | def get_data(): 17 | cursor = collection.find().limit(3*60*60).sort('_id', pymongo.DESCENDING) 18 | data = pd.DataFrame(list(cursor)) 19 | data = data.set_index('_id') 20 | data = data.sort_index(ascending=True) 21 | timestamps = pd.to_datetime(data.index, unit='s').to_series() 22 | prices = data.price 23 | predictions = data.prediction*10000 24 | returns = (data.position*data.change).cumsum()*10000 25 | return timestamps, prices, predictions, returns 26 | 27 | timestamps, prices, predictions, returns = get_data() 28 | output_server('bitpredict_extended') 29 | 30 | background = '#f2f2f2' 31 | ylabel_standoff = 0 32 | xformatter = DatetimeTickFormatter(formats=dict(hours=["%H:%M"])) 33 | yformatter = PrintfTickFormatter(format="%8.1f") 34 | p1 = figure(title=None, 35 | plot_width=750, 36 | plot_height=300, 37 | x_axis_type='datetime', 38 | min_border_top=10, 39 | min_border_bottom=33, 40 | background_fill=background, 41 | tools='', 42 | toolbar_location=None) 43 | p1.line(x=timestamps, 44 | y=prices, 45 | name='prices', 46 | color='#4271ae', 47 | line_width=1, 48 | legend='Bitcoin Bid/Ask Midpoint', 49 | line_cap='round', 50 | line_join='round') 51 | p1.legend.orientation = 'top_left' 52 | p1.legend.border_line_color = background 53 | p1.outline_line_color = None 54 | p1.xgrid.grid_line_color = 'white' 55 | p1.ygrid.grid_line_color = 'white' 56 | p1.axis.axis_line_color = None 57 | p1.axis.major_tick_line_color = None 58 | p1.axis.minor_tick_line_color = None 59 | p1.yaxis.axis_label = 'Price' 60 | p1.yaxis.axis_label_standoff = ylabel_standoff 61 | p1.xaxis.formatter = xformatter 62 | p1.yaxis.formatter = PrintfTickFormatter(format='%8.2f') 63 | p1.yaxis.major_label_text_font = 'courier' 64 | p1.xaxis.major_label_text_font = 'courier' 65 | 66 | p2 = figure(title=None, 67 | plot_width=750, 68 | plot_height=295, 69 | x_axis_type='datetime', 70 | min_border_top=5, 71 | min_border_bottom=33, 72 | background_fill=background, 73 | tools='', 74 | toolbar_location=None) 75 | p2.line(x=timestamps, 76 | y=predictions, 77 | name='predictions', 78 | color='#c82829', 79 | line_width=1, 80 | legend='30 Second Prediction', 81 | line_cap='round', 82 | line_join='round') 83 | p2.legend.orientation = 'top_left' 84 | p2.legend.border_line_color = background 85 | p2.outline_line_color = None 86 | p2.xgrid.grid_line_color = 'white' 87 | p2.ygrid.grid_line_color = 'white' 88 | p2.axis.axis_line_color = None 89 | p2.axis.major_tick_line_color = None 90 | p2.axis.minor_tick_line_color = None 91 | p2.yaxis.axis_label = 'Basis Points' 92 | p2.yaxis.axis_label_standoff = ylabel_standoff 93 | p2.xaxis.formatter = xformatter 94 | p2.yaxis.formatter = yformatter 95 | p2.yaxis.major_label_text_font = 'courier' 96 | p2.xaxis.major_label_text_font = 'courier' 97 | p2.x_range = p1.x_range 98 | 99 | p3 = figure(title=None, 100 | plot_width=750, 101 | plot_height=320, 102 | x_axis_type='datetime', 103 | min_border_top=5, 104 | min_border_bottom=10, 105 | background_fill=background, 106 | x_axis_label='Greenwich Mean Time', 107 | tools='', 108 | toolbar_location=None) 109 | p3.line(x=timestamps, 110 | y=returns, 111 | name='returns', 112 | color='#8959a8', 113 | line_width=1, 114 | legend='Cumulative Return', 115 | line_cap='round', 116 | line_join='round') 117 | p3.legend.orientation = 'top_left' 118 | p3.legend.border_line_color = background 119 | p3.outline_line_color = None 120 | p3.xgrid.grid_line_color = 'white' 121 | p3.ygrid.grid_line_color = 'white' 122 | p3.axis.axis_line_color = None 123 | p3.axis.major_tick_line_color = None 124 | p3.axis.minor_tick_line_color = None 125 | p3.yaxis.axis_label = 'Basis Points' 126 | p3.yaxis.axis_label_standoff = ylabel_standoff 127 | p3.xaxis.formatter = xformatter 128 | p3.yaxis.formatter = yformatter 129 | p3.xaxis.axis_label_standoff = 12 130 | p3.yaxis.major_label_text_font = 'courier' 131 | p3.xaxis.major_label_text_font = 'courier' 132 | p3.x_range = p1.x_range 133 | 134 | vp = vplot(p1, p2, p3) 135 | push() 136 | ip = load(urlopen('http://jsonip.com'))['ip'] 137 | ssn = cursession() 138 | ssn.publish() 139 | tag = embed.autoload_server(vp, ssn, public=True).replace('localhost', ip) 140 | html = """ 141 | {%% extends "layout.html" %%} 142 | {%% block bokeh %%} 143 | %s 144 | {%% endblock %%} 145 | """ % tag 146 | with open('templates/extended.html', 'w+') as f: 147 | f.write(html) 148 | 149 | renderer = p1.select(dict(name='prices')) 150 | ds_prices = renderer[0].data_source 151 | renderer = p2.select(dict(name='predictions')) 152 | ds_predictions = renderer[0].data_source 153 | renderer = p3.select(dict(name='returns')) 154 | ds_returns = renderer[0].data_source 155 | 156 | while True: 157 | timestamps, prices, predictions, returns = get_data() 158 | ds_prices.data['x'] = timestamps 159 | ds_predictions.data['x'] = timestamps 160 | ds_returns.data['x'] = timestamps 161 | ds_prices.data['y'] = prices 162 | ds_predictions.data['y'] = predictions 163 | ds_returns.data['y'] = returns 164 | ssn.store_objects(ds_prices) 165 | ssn.store_objects(ds_predictions) 166 | ssn.store_objects(ds_returns) 167 | time.sleep(60) 168 | -------------------------------------------------------------------------------- /app/run_charts_performance.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import pymongo 3 | from bokeh.plotting import cursession, figure, output_server, push 4 | from bokeh.models.formatters import DatetimeTickFormatter, PrintfTickFormatter 5 | from bokeh import embed 6 | from json import load 7 | from urllib2 import urlopen 8 | import time 9 | 10 | client = pymongo.MongoClient() 11 | db = client['bitmicro'] 12 | collection = db['btc_performance'] 13 | 14 | 15 | def get_data(): 16 | cursor = collection.find() 17 | data = pd.DataFrame(list(cursor)) 18 | data = data.set_index('_id') 19 | data = data.sort_index(ascending=True) 20 | timestamps = pd.to_datetime(data.index, unit='s').to_series() 21 | returns = data.returns.cumsum()*100 22 | return timestamps, returns 23 | 24 | timestamps, returns = get_data() 25 | output_server('bitpredict_performance') 26 | 27 | background = '#f2f2f2' 28 | ylabel_standoff = 0 29 | xformatter = DatetimeTickFormatter(formats=dict(hours=["%H:%M"])) 30 | yformatter = PrintfTickFormatter(format="%8.1f") 31 | 32 | p = figure(title=None, 33 | plot_width=750, 34 | plot_height=500, 35 | x_axis_type='datetime', 36 | min_border_top=5, 37 | min_border_bottom=10, 38 | background_fill=background, 39 | x_axis_label='Date', 40 | tools='', 41 | toolbar_location=None) 42 | p.line(x=timestamps, 43 | y=returns, 44 | name='returns', 45 | color='#8959a8', 46 | line_width=1, 47 | legend='Cumulative Return', 48 | line_cap='round', 49 | line_join='round') 50 | p.legend.orientation = 'top_left' 51 | p.legend.border_line_color = background 52 | p.outline_line_color = None 53 | p.xgrid.grid_line_color = 'white' 54 | p.ygrid.grid_line_color = 'white' 55 | p.axis.axis_line_color = None 56 | p.axis.major_tick_line_color = None 57 | p.axis.minor_tick_line_color = None 58 | p.yaxis.axis_label = 'Percent' 59 | p.yaxis.axis_label_standoff = ylabel_standoff 60 | p.xaxis.formatter = xformatter 61 | p.yaxis.formatter = yformatter 62 | p.xaxis.axis_label_standoff = 12 63 | p.yaxis.major_label_text_font = 'courier' 64 | p.xaxis.major_label_text_font = 'courier' 65 | 66 | push() 67 | ip = load(urlopen('http://jsonip.com'))['ip'] 68 | ssn = cursession() 69 | ssn.publish() 70 | tag = embed.autoload_server(p, ssn, public=True).replace('localhost', ip) 71 | html = """ 72 | {%% extends "layout.html" %%} 73 | {%% block bokeh %%} 74 | %s 75 | {%% endblock %%} 76 | """ % tag 77 | 78 | with open('templates/performance.html', 'w+') as f: 79 | f.write(html) 80 | 81 | renderer = p.select(dict(name='returns')) 82 | ds_returns = renderer[0].data_source 83 | 84 | while True: 85 | timestamps, returns = get_data() 86 | ds_returns.data['x'] = timestamps 87 | ds_returns.data['y'] = returns 88 | ssn.store_objects(ds_returns) 89 | time.sleep(300) 90 | -------------------------------------------------------------------------------- /app/static/css/portfolio-item.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - Portfolio Item HTML Template (http://startbootstrap.com) 3 | * Code licensed under the Apache License v2.0. 4 | * For details, see http://www.apache.org/licenses/LICENSE-2.0. 5 | */ 6 | 7 | body { 8 | padding-top: 35px; /* Required padding for .navbar-fixed-top. Remove if using .navbar-static-top. Change if height of navigation changes. */ 9 | } 10 | 11 | .portfolio-item { 12 | margin-bottom: 25px; 13 | } 14 | 15 | footer { 16 | margin: 50px 0; 17 | } 18 | -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/app/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/app/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/app/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/app/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /app/static/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | if (typeof jQuery === 'undefined') { 8 | throw new Error('Bootstrap\'s JavaScript requires jQuery') 9 | } 10 | 11 | +function ($) { 12 | 'use strict'; 13 | var version = $.fn.jquery.split(' ')[0].split('.') 14 | if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) { 15 | throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher') 16 | } 17 | }(jQuery); 18 | 19 | /* ======================================================================== 20 | * Bootstrap: transition.js v3.3.4 21 | * http://getbootstrap.com/javascript/#transitions 22 | * ======================================================================== 23 | * Copyright 2011-2015 Twitter, Inc. 24 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 25 | * ======================================================================== */ 26 | 27 | 28 | +function ($) { 29 | 'use strict'; 30 | 31 | // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) 32 | // ============================================================ 33 | 34 | function transitionEnd() { 35 | var el = document.createElement('bootstrap') 36 | 37 | var transEndEventNames = { 38 | WebkitTransition : 'webkitTransitionEnd', 39 | MozTransition : 'transitionend', 40 | OTransition : 'oTransitionEnd otransitionend', 41 | transition : 'transitionend' 42 | } 43 | 44 | for (var name in transEndEventNames) { 45 | if (el.style[name] !== undefined) { 46 | return { end: transEndEventNames[name] } 47 | } 48 | } 49 | 50 | return false // explicit for ie8 ( ._.) 51 | } 52 | 53 | // http://blog.alexmaccaw.com/css-transitions 54 | $.fn.emulateTransitionEnd = function (duration) { 55 | var called = false 56 | var $el = this 57 | $(this).one('bsTransitionEnd', function () { called = true }) 58 | var callback = function () { if (!called) $($el).trigger($.support.transition.end) } 59 | setTimeout(callback, duration) 60 | return this 61 | } 62 | 63 | $(function () { 64 | $.support.transition = transitionEnd() 65 | 66 | if (!$.support.transition) return 67 | 68 | $.event.special.bsTransitionEnd = { 69 | bindType: $.support.transition.end, 70 | delegateType: $.support.transition.end, 71 | handle: function (e) { 72 | if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) 73 | } 74 | } 75 | }) 76 | 77 | }(jQuery); 78 | 79 | /* ======================================================================== 80 | * Bootstrap: alert.js v3.3.4 81 | * http://getbootstrap.com/javascript/#alerts 82 | * ======================================================================== 83 | * Copyright 2011-2015 Twitter, Inc. 84 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 85 | * ======================================================================== */ 86 | 87 | 88 | +function ($) { 89 | 'use strict'; 90 | 91 | // ALERT CLASS DEFINITION 92 | // ====================== 93 | 94 | var dismiss = '[data-dismiss="alert"]' 95 | var Alert = function (el) { 96 | $(el).on('click', dismiss, this.close) 97 | } 98 | 99 | Alert.VERSION = '3.3.4' 100 | 101 | Alert.TRANSITION_DURATION = 150 102 | 103 | Alert.prototype.close = function (e) { 104 | var $this = $(this) 105 | var selector = $this.attr('data-target') 106 | 107 | if (!selector) { 108 | selector = $this.attr('href') 109 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 110 | } 111 | 112 | var $parent = $(selector) 113 | 114 | if (e) e.preventDefault() 115 | 116 | if (!$parent.length) { 117 | $parent = $this.closest('.alert') 118 | } 119 | 120 | $parent.trigger(e = $.Event('close.bs.alert')) 121 | 122 | if (e.isDefaultPrevented()) return 123 | 124 | $parent.removeClass('in') 125 | 126 | function removeElement() { 127 | // detach from parent, fire event then clean up data 128 | $parent.detach().trigger('closed.bs.alert').remove() 129 | } 130 | 131 | $.support.transition && $parent.hasClass('fade') ? 132 | $parent 133 | .one('bsTransitionEnd', removeElement) 134 | .emulateTransitionEnd(Alert.TRANSITION_DURATION) : 135 | removeElement() 136 | } 137 | 138 | 139 | // ALERT PLUGIN DEFINITION 140 | // ======================= 141 | 142 | function Plugin(option) { 143 | return this.each(function () { 144 | var $this = $(this) 145 | var data = $this.data('bs.alert') 146 | 147 | if (!data) $this.data('bs.alert', (data = new Alert(this))) 148 | if (typeof option == 'string') data[option].call($this) 149 | }) 150 | } 151 | 152 | var old = $.fn.alert 153 | 154 | $.fn.alert = Plugin 155 | $.fn.alert.Constructor = Alert 156 | 157 | 158 | // ALERT NO CONFLICT 159 | // ================= 160 | 161 | $.fn.alert.noConflict = function () { 162 | $.fn.alert = old 163 | return this 164 | } 165 | 166 | 167 | // ALERT DATA-API 168 | // ============== 169 | 170 | $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) 171 | 172 | }(jQuery); 173 | 174 | /* ======================================================================== 175 | * Bootstrap: button.js v3.3.4 176 | * http://getbootstrap.com/javascript/#buttons 177 | * ======================================================================== 178 | * Copyright 2011-2015 Twitter, Inc. 179 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 180 | * ======================================================================== */ 181 | 182 | 183 | +function ($) { 184 | 'use strict'; 185 | 186 | // BUTTON PUBLIC CLASS DEFINITION 187 | // ============================== 188 | 189 | var Button = function (element, options) { 190 | this.$element = $(element) 191 | this.options = $.extend({}, Button.DEFAULTS, options) 192 | this.isLoading = false 193 | } 194 | 195 | Button.VERSION = '3.3.4' 196 | 197 | Button.DEFAULTS = { 198 | loadingText: 'loading...' 199 | } 200 | 201 | Button.prototype.setState = function (state) { 202 | var d = 'disabled' 203 | var $el = this.$element 204 | var val = $el.is('input') ? 'val' : 'html' 205 | var data = $el.data() 206 | 207 | state = state + 'Text' 208 | 209 | if (data.resetText == null) $el.data('resetText', $el[val]()) 210 | 211 | // push to event loop to allow forms to submit 212 | setTimeout($.proxy(function () { 213 | $el[val](data[state] == null ? this.options[state] : data[state]) 214 | 215 | if (state == 'loadingText') { 216 | this.isLoading = true 217 | $el.addClass(d).attr(d, d) 218 | } else if (this.isLoading) { 219 | this.isLoading = false 220 | $el.removeClass(d).removeAttr(d) 221 | } 222 | }, this), 0) 223 | } 224 | 225 | Button.prototype.toggle = function () { 226 | var changed = true 227 | var $parent = this.$element.closest('[data-toggle="buttons"]') 228 | 229 | if ($parent.length) { 230 | var $input = this.$element.find('input') 231 | if ($input.prop('type') == 'radio') { 232 | if ($input.prop('checked') && this.$element.hasClass('active')) changed = false 233 | else $parent.find('.active').removeClass('active') 234 | } 235 | if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') 236 | } else { 237 | this.$element.attr('aria-pressed', !this.$element.hasClass('active')) 238 | } 239 | 240 | if (changed) this.$element.toggleClass('active') 241 | } 242 | 243 | 244 | // BUTTON PLUGIN DEFINITION 245 | // ======================== 246 | 247 | function Plugin(option) { 248 | return this.each(function () { 249 | var $this = $(this) 250 | var data = $this.data('bs.button') 251 | var options = typeof option == 'object' && option 252 | 253 | if (!data) $this.data('bs.button', (data = new Button(this, options))) 254 | 255 | if (option == 'toggle') data.toggle() 256 | else if (option) data.setState(option) 257 | }) 258 | } 259 | 260 | var old = $.fn.button 261 | 262 | $.fn.button = Plugin 263 | $.fn.button.Constructor = Button 264 | 265 | 266 | // BUTTON NO CONFLICT 267 | // ================== 268 | 269 | $.fn.button.noConflict = function () { 270 | $.fn.button = old 271 | return this 272 | } 273 | 274 | 275 | // BUTTON DATA-API 276 | // =============== 277 | 278 | $(document) 279 | .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { 280 | var $btn = $(e.target) 281 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 282 | Plugin.call($btn, 'toggle') 283 | e.preventDefault() 284 | }) 285 | .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { 286 | $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) 287 | }) 288 | 289 | }(jQuery); 290 | 291 | /* ======================================================================== 292 | * Bootstrap: carousel.js v3.3.4 293 | * http://getbootstrap.com/javascript/#carousel 294 | * ======================================================================== 295 | * Copyright 2011-2015 Twitter, Inc. 296 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 297 | * ======================================================================== */ 298 | 299 | 300 | +function ($) { 301 | 'use strict'; 302 | 303 | // CAROUSEL CLASS DEFINITION 304 | // ========================= 305 | 306 | var Carousel = function (element, options) { 307 | this.$element = $(element) 308 | this.$indicators = this.$element.find('.carousel-indicators') 309 | this.options = options 310 | this.paused = null 311 | this.sliding = null 312 | this.interval = null 313 | this.$active = null 314 | this.$items = null 315 | 316 | this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) 317 | 318 | this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element 319 | .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) 320 | .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) 321 | } 322 | 323 | Carousel.VERSION = '3.3.4' 324 | 325 | Carousel.TRANSITION_DURATION = 600 326 | 327 | Carousel.DEFAULTS = { 328 | interval: 5000, 329 | pause: 'hover', 330 | wrap: true, 331 | keyboard: true 332 | } 333 | 334 | Carousel.prototype.keydown = function (e) { 335 | if (/input|textarea/i.test(e.target.tagName)) return 336 | switch (e.which) { 337 | case 37: this.prev(); break 338 | case 39: this.next(); break 339 | default: return 340 | } 341 | 342 | e.preventDefault() 343 | } 344 | 345 | Carousel.prototype.cycle = function (e) { 346 | e || (this.paused = false) 347 | 348 | this.interval && clearInterval(this.interval) 349 | 350 | this.options.interval 351 | && !this.paused 352 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) 353 | 354 | return this 355 | } 356 | 357 | Carousel.prototype.getItemIndex = function (item) { 358 | this.$items = item.parent().children('.item') 359 | return this.$items.index(item || this.$active) 360 | } 361 | 362 | Carousel.prototype.getItemForDirection = function (direction, active) { 363 | var activeIndex = this.getItemIndex(active) 364 | var willWrap = (direction == 'prev' && activeIndex === 0) 365 | || (direction == 'next' && activeIndex == (this.$items.length - 1)) 366 | if (willWrap && !this.options.wrap) return active 367 | var delta = direction == 'prev' ? -1 : 1 368 | var itemIndex = (activeIndex + delta) % this.$items.length 369 | return this.$items.eq(itemIndex) 370 | } 371 | 372 | Carousel.prototype.to = function (pos) { 373 | var that = this 374 | var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) 375 | 376 | if (pos > (this.$items.length - 1) || pos < 0) return 377 | 378 | if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" 379 | if (activeIndex == pos) return this.pause().cycle() 380 | 381 | return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) 382 | } 383 | 384 | Carousel.prototype.pause = function (e) { 385 | e || (this.paused = true) 386 | 387 | if (this.$element.find('.next, .prev').length && $.support.transition) { 388 | this.$element.trigger($.support.transition.end) 389 | this.cycle(true) 390 | } 391 | 392 | this.interval = clearInterval(this.interval) 393 | 394 | return this 395 | } 396 | 397 | Carousel.prototype.next = function () { 398 | if (this.sliding) return 399 | return this.slide('next') 400 | } 401 | 402 | Carousel.prototype.prev = function () { 403 | if (this.sliding) return 404 | return this.slide('prev') 405 | } 406 | 407 | Carousel.prototype.slide = function (type, next) { 408 | var $active = this.$element.find('.item.active') 409 | var $next = next || this.getItemForDirection(type, $active) 410 | var isCycling = this.interval 411 | var direction = type == 'next' ? 'left' : 'right' 412 | var that = this 413 | 414 | if ($next.hasClass('active')) return (this.sliding = false) 415 | 416 | var relatedTarget = $next[0] 417 | var slideEvent = $.Event('slide.bs.carousel', { 418 | relatedTarget: relatedTarget, 419 | direction: direction 420 | }) 421 | this.$element.trigger(slideEvent) 422 | if (slideEvent.isDefaultPrevented()) return 423 | 424 | this.sliding = true 425 | 426 | isCycling && this.pause() 427 | 428 | if (this.$indicators.length) { 429 | this.$indicators.find('.active').removeClass('active') 430 | var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) 431 | $nextIndicator && $nextIndicator.addClass('active') 432 | } 433 | 434 | var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" 435 | if ($.support.transition && this.$element.hasClass('slide')) { 436 | $next.addClass(type) 437 | $next[0].offsetWidth // force reflow 438 | $active.addClass(direction) 439 | $next.addClass(direction) 440 | $active 441 | .one('bsTransitionEnd', function () { 442 | $next.removeClass([type, direction].join(' ')).addClass('active') 443 | $active.removeClass(['active', direction].join(' ')) 444 | that.sliding = false 445 | setTimeout(function () { 446 | that.$element.trigger(slidEvent) 447 | }, 0) 448 | }) 449 | .emulateTransitionEnd(Carousel.TRANSITION_DURATION) 450 | } else { 451 | $active.removeClass('active') 452 | $next.addClass('active') 453 | this.sliding = false 454 | this.$element.trigger(slidEvent) 455 | } 456 | 457 | isCycling && this.cycle() 458 | 459 | return this 460 | } 461 | 462 | 463 | // CAROUSEL PLUGIN DEFINITION 464 | // ========================== 465 | 466 | function Plugin(option) { 467 | return this.each(function () { 468 | var $this = $(this) 469 | var data = $this.data('bs.carousel') 470 | var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) 471 | var action = typeof option == 'string' ? option : options.slide 472 | 473 | if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) 474 | if (typeof option == 'number') data.to(option) 475 | else if (action) data[action]() 476 | else if (options.interval) data.pause().cycle() 477 | }) 478 | } 479 | 480 | var old = $.fn.carousel 481 | 482 | $.fn.carousel = Plugin 483 | $.fn.carousel.Constructor = Carousel 484 | 485 | 486 | // CAROUSEL NO CONFLICT 487 | // ==================== 488 | 489 | $.fn.carousel.noConflict = function () { 490 | $.fn.carousel = old 491 | return this 492 | } 493 | 494 | 495 | // CAROUSEL DATA-API 496 | // ================= 497 | 498 | var clickHandler = function (e) { 499 | var href 500 | var $this = $(this) 501 | var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 502 | if (!$target.hasClass('carousel')) return 503 | var options = $.extend({}, $target.data(), $this.data()) 504 | var slideIndex = $this.attr('data-slide-to') 505 | if (slideIndex) options.interval = false 506 | 507 | Plugin.call($target, options) 508 | 509 | if (slideIndex) { 510 | $target.data('bs.carousel').to(slideIndex) 511 | } 512 | 513 | e.preventDefault() 514 | } 515 | 516 | $(document) 517 | .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) 518 | .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) 519 | 520 | $(window).on('load', function () { 521 | $('[data-ride="carousel"]').each(function () { 522 | var $carousel = $(this) 523 | Plugin.call($carousel, $carousel.data()) 524 | }) 525 | }) 526 | 527 | }(jQuery); 528 | 529 | /* ======================================================================== 530 | * Bootstrap: collapse.js v3.3.4 531 | * http://getbootstrap.com/javascript/#collapse 532 | * ======================================================================== 533 | * Copyright 2011-2015 Twitter, Inc. 534 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 535 | * ======================================================================== */ 536 | 537 | 538 | +function ($) { 539 | 'use strict'; 540 | 541 | // COLLAPSE PUBLIC CLASS DEFINITION 542 | // ================================ 543 | 544 | var Collapse = function (element, options) { 545 | this.$element = $(element) 546 | this.options = $.extend({}, Collapse.DEFAULTS, options) 547 | this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + 548 | '[data-toggle="collapse"][data-target="#' + element.id + '"]') 549 | this.transitioning = null 550 | 551 | if (this.options.parent) { 552 | this.$parent = this.getParent() 553 | } else { 554 | this.addAriaAndCollapsedClass(this.$element, this.$trigger) 555 | } 556 | 557 | if (this.options.toggle) this.toggle() 558 | } 559 | 560 | Collapse.VERSION = '3.3.4' 561 | 562 | Collapse.TRANSITION_DURATION = 350 563 | 564 | Collapse.DEFAULTS = { 565 | toggle: true 566 | } 567 | 568 | Collapse.prototype.dimension = function () { 569 | var hasWidth = this.$element.hasClass('width') 570 | return hasWidth ? 'width' : 'height' 571 | } 572 | 573 | Collapse.prototype.show = function () { 574 | if (this.transitioning || this.$element.hasClass('in')) return 575 | 576 | var activesData 577 | var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') 578 | 579 | if (actives && actives.length) { 580 | activesData = actives.data('bs.collapse') 581 | if (activesData && activesData.transitioning) return 582 | } 583 | 584 | var startEvent = $.Event('show.bs.collapse') 585 | this.$element.trigger(startEvent) 586 | if (startEvent.isDefaultPrevented()) return 587 | 588 | if (actives && actives.length) { 589 | Plugin.call(actives, 'hide') 590 | activesData || actives.data('bs.collapse', null) 591 | } 592 | 593 | var dimension = this.dimension() 594 | 595 | this.$element 596 | .removeClass('collapse') 597 | .addClass('collapsing')[dimension](0) 598 | .attr('aria-expanded', true) 599 | 600 | this.$trigger 601 | .removeClass('collapsed') 602 | .attr('aria-expanded', true) 603 | 604 | this.transitioning = 1 605 | 606 | var complete = function () { 607 | this.$element 608 | .removeClass('collapsing') 609 | .addClass('collapse in')[dimension]('') 610 | this.transitioning = 0 611 | this.$element 612 | .trigger('shown.bs.collapse') 613 | } 614 | 615 | if (!$.support.transition) return complete.call(this) 616 | 617 | var scrollSize = $.camelCase(['scroll', dimension].join('-')) 618 | 619 | this.$element 620 | .one('bsTransitionEnd', $.proxy(complete, this)) 621 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) 622 | } 623 | 624 | Collapse.prototype.hide = function () { 625 | if (this.transitioning || !this.$element.hasClass('in')) return 626 | 627 | var startEvent = $.Event('hide.bs.collapse') 628 | this.$element.trigger(startEvent) 629 | if (startEvent.isDefaultPrevented()) return 630 | 631 | var dimension = this.dimension() 632 | 633 | this.$element[dimension](this.$element[dimension]())[0].offsetHeight 634 | 635 | this.$element 636 | .addClass('collapsing') 637 | .removeClass('collapse in') 638 | .attr('aria-expanded', false) 639 | 640 | this.$trigger 641 | .addClass('collapsed') 642 | .attr('aria-expanded', false) 643 | 644 | this.transitioning = 1 645 | 646 | var complete = function () { 647 | this.transitioning = 0 648 | this.$element 649 | .removeClass('collapsing') 650 | .addClass('collapse') 651 | .trigger('hidden.bs.collapse') 652 | } 653 | 654 | if (!$.support.transition) return complete.call(this) 655 | 656 | this.$element 657 | [dimension](0) 658 | .one('bsTransitionEnd', $.proxy(complete, this)) 659 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION) 660 | } 661 | 662 | Collapse.prototype.toggle = function () { 663 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 664 | } 665 | 666 | Collapse.prototype.getParent = function () { 667 | return $(this.options.parent) 668 | .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') 669 | .each($.proxy(function (i, element) { 670 | var $element = $(element) 671 | this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) 672 | }, this)) 673 | .end() 674 | } 675 | 676 | Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { 677 | var isOpen = $element.hasClass('in') 678 | 679 | $element.attr('aria-expanded', isOpen) 680 | $trigger 681 | .toggleClass('collapsed', !isOpen) 682 | .attr('aria-expanded', isOpen) 683 | } 684 | 685 | function getTargetFromTrigger($trigger) { 686 | var href 687 | var target = $trigger.attr('data-target') 688 | || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 689 | 690 | return $(target) 691 | } 692 | 693 | 694 | // COLLAPSE PLUGIN DEFINITION 695 | // ========================== 696 | 697 | function Plugin(option) { 698 | return this.each(function () { 699 | var $this = $(this) 700 | var data = $this.data('bs.collapse') 701 | var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) 702 | 703 | if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false 704 | if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) 705 | if (typeof option == 'string') data[option]() 706 | }) 707 | } 708 | 709 | var old = $.fn.collapse 710 | 711 | $.fn.collapse = Plugin 712 | $.fn.collapse.Constructor = Collapse 713 | 714 | 715 | // COLLAPSE NO CONFLICT 716 | // ==================== 717 | 718 | $.fn.collapse.noConflict = function () { 719 | $.fn.collapse = old 720 | return this 721 | } 722 | 723 | 724 | // COLLAPSE DATA-API 725 | // ================= 726 | 727 | $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { 728 | var $this = $(this) 729 | 730 | if (!$this.attr('data-target')) e.preventDefault() 731 | 732 | var $target = getTargetFromTrigger($this) 733 | var data = $target.data('bs.collapse') 734 | var option = data ? 'toggle' : $this.data() 735 | 736 | Plugin.call($target, option) 737 | }) 738 | 739 | }(jQuery); 740 | 741 | /* ======================================================================== 742 | * Bootstrap: dropdown.js v3.3.4 743 | * http://getbootstrap.com/javascript/#dropdowns 744 | * ======================================================================== 745 | * Copyright 2011-2015 Twitter, Inc. 746 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 747 | * ======================================================================== */ 748 | 749 | 750 | +function ($) { 751 | 'use strict'; 752 | 753 | // DROPDOWN CLASS DEFINITION 754 | // ========================= 755 | 756 | var backdrop = '.dropdown-backdrop' 757 | var toggle = '[data-toggle="dropdown"]' 758 | var Dropdown = function (element) { 759 | $(element).on('click.bs.dropdown', this.toggle) 760 | } 761 | 762 | Dropdown.VERSION = '3.3.4' 763 | 764 | Dropdown.prototype.toggle = function (e) { 765 | var $this = $(this) 766 | 767 | if ($this.is('.disabled, :disabled')) return 768 | 769 | var $parent = getParent($this) 770 | var isActive = $parent.hasClass('open') 771 | 772 | clearMenus() 773 | 774 | if (!isActive) { 775 | if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { 776 | // if mobile we use a backdrop because click events don't delegate 777 | $('