├── .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 | 
43 |
44 | 
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 | 
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 | $('
').insertAfter($(this)).on('click', clearMenus)
778 | }
779 |
780 | var relatedTarget = { relatedTarget: this }
781 | $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))
782 |
783 | if (e.isDefaultPrevented()) return
784 |
785 | $this
786 | .trigger('focus')
787 | .attr('aria-expanded', 'true')
788 |
789 | $parent
790 | .toggleClass('open')
791 | .trigger('shown.bs.dropdown', relatedTarget)
792 | }
793 |
794 | return false
795 | }
796 |
797 | Dropdown.prototype.keydown = function (e) {
798 | if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return
799 |
800 | var $this = $(this)
801 |
802 | e.preventDefault()
803 | e.stopPropagation()
804 |
805 | if ($this.is('.disabled, :disabled')) return
806 |
807 | var $parent = getParent($this)
808 | var isActive = $parent.hasClass('open')
809 |
810 | if ((!isActive && e.which != 27) || (isActive && e.which == 27)) {
811 | if (e.which == 27) $parent.find(toggle).trigger('focus')
812 | return $this.trigger('click')
813 | }
814 |
815 | var desc = ' li:not(.disabled):visible a'
816 | var $items = $parent.find('[role="menu"]' + desc + ', [role="listbox"]' + desc)
817 |
818 | if (!$items.length) return
819 |
820 | var index = $items.index(e.target)
821 |
822 | if (e.which == 38 && index > 0) index-- // up
823 | if (e.which == 40 && index < $items.length - 1) index++ // down
824 | if (!~index) index = 0
825 |
826 | $items.eq(index).trigger('focus')
827 | }
828 |
829 | function clearMenus(e) {
830 | if (e && e.which === 3) return
831 | $(backdrop).remove()
832 | $(toggle).each(function () {
833 | var $this = $(this)
834 | var $parent = getParent($this)
835 | var relatedTarget = { relatedTarget: this }
836 |
837 | if (!$parent.hasClass('open')) return
838 |
839 | $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
840 |
841 | if (e.isDefaultPrevented()) return
842 |
843 | $this.attr('aria-expanded', 'false')
844 | $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget)
845 | })
846 | }
847 |
848 | function getParent($this) {
849 | var selector = $this.attr('data-target')
850 |
851 | if (!selector) {
852 | selector = $this.attr('href')
853 | selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
854 | }
855 |
856 | var $parent = selector && $(selector)
857 |
858 | return $parent && $parent.length ? $parent : $this.parent()
859 | }
860 |
861 |
862 | // DROPDOWN PLUGIN DEFINITION
863 | // ==========================
864 |
865 | function Plugin(option) {
866 | return this.each(function () {
867 | var $this = $(this)
868 | var data = $this.data('bs.dropdown')
869 |
870 | if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
871 | if (typeof option == 'string') data[option].call($this)
872 | })
873 | }
874 |
875 | var old = $.fn.dropdown
876 |
877 | $.fn.dropdown = Plugin
878 | $.fn.dropdown.Constructor = Dropdown
879 |
880 |
881 | // DROPDOWN NO CONFLICT
882 | // ====================
883 |
884 | $.fn.dropdown.noConflict = function () {
885 | $.fn.dropdown = old
886 | return this
887 | }
888 |
889 |
890 | // APPLY TO STANDARD DROPDOWN ELEMENTS
891 | // ===================================
892 |
893 | $(document)
894 | .on('click.bs.dropdown.data-api', clearMenus)
895 | .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
896 | .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
897 | .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
898 | .on('keydown.bs.dropdown.data-api', '[role="menu"]', Dropdown.prototype.keydown)
899 | .on('keydown.bs.dropdown.data-api', '[role="listbox"]', Dropdown.prototype.keydown)
900 |
901 | }(jQuery);
902 |
903 | /* ========================================================================
904 | * Bootstrap: modal.js v3.3.4
905 | * http://getbootstrap.com/javascript/#modals
906 | * ========================================================================
907 | * Copyright 2011-2015 Twitter, Inc.
908 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
909 | * ======================================================================== */
910 |
911 |
912 | +function ($) {
913 | 'use strict';
914 |
915 | // MODAL CLASS DEFINITION
916 | // ======================
917 |
918 | var Modal = function (element, options) {
919 | this.options = options
920 | this.$body = $(document.body)
921 | this.$element = $(element)
922 | this.$dialog = this.$element.find('.modal-dialog')
923 | this.$backdrop = null
924 | this.isShown = null
925 | this.originalBodyPad = null
926 | this.scrollbarWidth = 0
927 | this.ignoreBackdropClick = false
928 |
929 | if (this.options.remote) {
930 | this.$element
931 | .find('.modal-content')
932 | .load(this.options.remote, $.proxy(function () {
933 | this.$element.trigger('loaded.bs.modal')
934 | }, this))
935 | }
936 | }
937 |
938 | Modal.VERSION = '3.3.4'
939 |
940 | Modal.TRANSITION_DURATION = 300
941 | Modal.BACKDROP_TRANSITION_DURATION = 150
942 |
943 | Modal.DEFAULTS = {
944 | backdrop: true,
945 | keyboard: true,
946 | show: true
947 | }
948 |
949 | Modal.prototype.toggle = function (_relatedTarget) {
950 | return this.isShown ? this.hide() : this.show(_relatedTarget)
951 | }
952 |
953 | Modal.prototype.show = function (_relatedTarget) {
954 | var that = this
955 | var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
956 |
957 | this.$element.trigger(e)
958 |
959 | if (this.isShown || e.isDefaultPrevented()) return
960 |
961 | this.isShown = true
962 |
963 | this.checkScrollbar()
964 | this.setScrollbar()
965 | this.$body.addClass('modal-open')
966 |
967 | this.escape()
968 | this.resize()
969 |
970 | this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
971 |
972 | this.$dialog.on('mousedown.dismiss.bs.modal', function () {
973 | that.$element.one('mouseup.dismiss.bs.modal', function (e) {
974 | if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true
975 | })
976 | })
977 |
978 | this.backdrop(function () {
979 | var transition = $.support.transition && that.$element.hasClass('fade')
980 |
981 | if (!that.$element.parent().length) {
982 | that.$element.appendTo(that.$body) // don't move modals dom position
983 | }
984 |
985 | that.$element
986 | .show()
987 | .scrollTop(0)
988 |
989 | that.adjustDialog()
990 |
991 | if (transition) {
992 | that.$element[0].offsetWidth // force reflow
993 | }
994 |
995 | that.$element
996 | .addClass('in')
997 | .attr('aria-hidden', false)
998 |
999 | that.enforceFocus()
1000 |
1001 | var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
1002 |
1003 | transition ?
1004 | that.$dialog // wait for modal to slide in
1005 | .one('bsTransitionEnd', function () {
1006 | that.$element.trigger('focus').trigger(e)
1007 | })
1008 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) :
1009 | that.$element.trigger('focus').trigger(e)
1010 | })
1011 | }
1012 |
1013 | Modal.prototype.hide = function (e) {
1014 | if (e) e.preventDefault()
1015 |
1016 | e = $.Event('hide.bs.modal')
1017 |
1018 | this.$element.trigger(e)
1019 |
1020 | if (!this.isShown || e.isDefaultPrevented()) return
1021 |
1022 | this.isShown = false
1023 |
1024 | this.escape()
1025 | this.resize()
1026 |
1027 | $(document).off('focusin.bs.modal')
1028 |
1029 | this.$element
1030 | .removeClass('in')
1031 | .attr('aria-hidden', true)
1032 | .off('click.dismiss.bs.modal')
1033 | .off('mouseup.dismiss.bs.modal')
1034 |
1035 | this.$dialog.off('mousedown.dismiss.bs.modal')
1036 |
1037 | $.support.transition && this.$element.hasClass('fade') ?
1038 | this.$element
1039 | .one('bsTransitionEnd', $.proxy(this.hideModal, this))
1040 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) :
1041 | this.hideModal()
1042 | }
1043 |
1044 | Modal.prototype.enforceFocus = function () {
1045 | $(document)
1046 | .off('focusin.bs.modal') // guard against infinite focus loop
1047 | .on('focusin.bs.modal', $.proxy(function (e) {
1048 | if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
1049 | this.$element.trigger('focus')
1050 | }
1051 | }, this))
1052 | }
1053 |
1054 | Modal.prototype.escape = function () {
1055 | if (this.isShown && this.options.keyboard) {
1056 | this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) {
1057 | e.which == 27 && this.hide()
1058 | }, this))
1059 | } else if (!this.isShown) {
1060 | this.$element.off('keydown.dismiss.bs.modal')
1061 | }
1062 | }
1063 |
1064 | Modal.prototype.resize = function () {
1065 | if (this.isShown) {
1066 | $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this))
1067 | } else {
1068 | $(window).off('resize.bs.modal')
1069 | }
1070 | }
1071 |
1072 | Modal.prototype.hideModal = function () {
1073 | var that = this
1074 | this.$element.hide()
1075 | this.backdrop(function () {
1076 | that.$body.removeClass('modal-open')
1077 | that.resetAdjustments()
1078 | that.resetScrollbar()
1079 | that.$element.trigger('hidden.bs.modal')
1080 | })
1081 | }
1082 |
1083 | Modal.prototype.removeBackdrop = function () {
1084 | this.$backdrop && this.$backdrop.remove()
1085 | this.$backdrop = null
1086 | }
1087 |
1088 | Modal.prototype.backdrop = function (callback) {
1089 | var that = this
1090 | var animate = this.$element.hasClass('fade') ? 'fade' : ''
1091 |
1092 | if (this.isShown && this.options.backdrop) {
1093 | var doAnimate = $.support.transition && animate
1094 |
1095 | this.$backdrop = $('')
1096 | .appendTo(this.$body)
1097 |
1098 | this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) {
1099 | if (this.ignoreBackdropClick) {
1100 | this.ignoreBackdropClick = false
1101 | return
1102 | }
1103 | if (e.target !== e.currentTarget) return
1104 | this.options.backdrop == 'static'
1105 | ? this.$element[0].focus()
1106 | : this.hide()
1107 | }, this))
1108 |
1109 | if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
1110 |
1111 | this.$backdrop.addClass('in')
1112 |
1113 | if (!callback) return
1114 |
1115 | doAnimate ?
1116 | this.$backdrop
1117 | .one('bsTransitionEnd', callback)
1118 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
1119 | callback()
1120 |
1121 | } else if (!this.isShown && this.$backdrop) {
1122 | this.$backdrop.removeClass('in')
1123 |
1124 | var callbackRemove = function () {
1125 | that.removeBackdrop()
1126 | callback && callback()
1127 | }
1128 | $.support.transition && this.$element.hasClass('fade') ?
1129 | this.$backdrop
1130 | .one('bsTransitionEnd', callbackRemove)
1131 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
1132 | callbackRemove()
1133 |
1134 | } else if (callback) {
1135 | callback()
1136 | }
1137 | }
1138 |
1139 | // these following methods are used to handle overflowing modals
1140 |
1141 | Modal.prototype.handleUpdate = function () {
1142 | this.adjustDialog()
1143 | }
1144 |
1145 | Modal.prototype.adjustDialog = function () {
1146 | var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight
1147 |
1148 | this.$element.css({
1149 | paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '',
1150 | paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : ''
1151 | })
1152 | }
1153 |
1154 | Modal.prototype.resetAdjustments = function () {
1155 | this.$element.css({
1156 | paddingLeft: '',
1157 | paddingRight: ''
1158 | })
1159 | }
1160 |
1161 | Modal.prototype.checkScrollbar = function () {
1162 | var fullWindowWidth = window.innerWidth
1163 | if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8
1164 | var documentElementRect = document.documentElement.getBoundingClientRect()
1165 | fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left)
1166 | }
1167 | this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth
1168 | this.scrollbarWidth = this.measureScrollbar()
1169 | }
1170 |
1171 | Modal.prototype.setScrollbar = function () {
1172 | var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10)
1173 | this.originalBodyPad = document.body.style.paddingRight || ''
1174 | if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth)
1175 | }
1176 |
1177 | Modal.prototype.resetScrollbar = function () {
1178 | this.$body.css('padding-right', this.originalBodyPad)
1179 | }
1180 |
1181 | Modal.prototype.measureScrollbar = function () { // thx walsh
1182 | var scrollDiv = document.createElement('div')
1183 | scrollDiv.className = 'modal-scrollbar-measure'
1184 | this.$body.append(scrollDiv)
1185 | var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
1186 | this.$body[0].removeChild(scrollDiv)
1187 | return scrollbarWidth
1188 | }
1189 |
1190 |
1191 | // MODAL PLUGIN DEFINITION
1192 | // =======================
1193 |
1194 | function Plugin(option, _relatedTarget) {
1195 | return this.each(function () {
1196 | var $this = $(this)
1197 | var data = $this.data('bs.modal')
1198 | var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
1199 |
1200 | if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
1201 | if (typeof option == 'string') data[option](_relatedTarget)
1202 | else if (options.show) data.show(_relatedTarget)
1203 | })
1204 | }
1205 |
1206 | var old = $.fn.modal
1207 |
1208 | $.fn.modal = Plugin
1209 | $.fn.modal.Constructor = Modal
1210 |
1211 |
1212 | // MODAL NO CONFLICT
1213 | // =================
1214 |
1215 | $.fn.modal.noConflict = function () {
1216 | $.fn.modal = old
1217 | return this
1218 | }
1219 |
1220 |
1221 | // MODAL DATA-API
1222 | // ==============
1223 |
1224 | $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
1225 | var $this = $(this)
1226 | var href = $this.attr('href')
1227 | var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7
1228 | var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
1229 |
1230 | if ($this.is('a')) e.preventDefault()
1231 |
1232 | $target.one('show.bs.modal', function (showEvent) {
1233 | if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown
1234 | $target.one('hidden.bs.modal', function () {
1235 | $this.is(':visible') && $this.trigger('focus')
1236 | })
1237 | })
1238 | Plugin.call($target, option, this)
1239 | })
1240 |
1241 | }(jQuery);
1242 |
1243 | /* ========================================================================
1244 | * Bootstrap: tooltip.js v3.3.4
1245 | * http://getbootstrap.com/javascript/#tooltip
1246 | * Inspired by the original jQuery.tipsy by Jason Frame
1247 | * ========================================================================
1248 | * Copyright 2011-2015 Twitter, Inc.
1249 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
1250 | * ======================================================================== */
1251 |
1252 |
1253 | +function ($) {
1254 | 'use strict';
1255 |
1256 | // TOOLTIP PUBLIC CLASS DEFINITION
1257 | // ===============================
1258 |
1259 | var Tooltip = function (element, options) {
1260 | this.type = null
1261 | this.options = null
1262 | this.enabled = null
1263 | this.timeout = null
1264 | this.hoverState = null
1265 | this.$element = null
1266 |
1267 | this.init('tooltip', element, options)
1268 | }
1269 |
1270 | Tooltip.VERSION = '3.3.4'
1271 |
1272 | Tooltip.TRANSITION_DURATION = 150
1273 |
1274 | Tooltip.DEFAULTS = {
1275 | animation: true,
1276 | placement: 'top',
1277 | selector: false,
1278 | template: '',
1279 | trigger: 'hover focus',
1280 | title: '',
1281 | delay: 0,
1282 | html: false,
1283 | container: false,
1284 | viewport: {
1285 | selector: 'body',
1286 | padding: 0
1287 | }
1288 | }
1289 |
1290 | Tooltip.prototype.init = function (type, element, options) {
1291 | this.enabled = true
1292 | this.type = type
1293 | this.$element = $(element)
1294 | this.options = this.getOptions(options)
1295 | this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)
1296 |
1297 | if (this.$element[0] instanceof document.constructor && !this.options.selector) {
1298 | throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!')
1299 | }
1300 |
1301 | var triggers = this.options.trigger.split(' ')
1302 |
1303 | for (var i = triggers.length; i--;) {
1304 | var trigger = triggers[i]
1305 |
1306 | if (trigger == 'click') {
1307 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
1308 | } else if (trigger != 'manual') {
1309 | var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
1310 | var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
1311 |
1312 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
1313 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
1314 | }
1315 | }
1316 |
1317 | this.options.selector ?
1318 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
1319 | this.fixTitle()
1320 | }
1321 |
1322 | Tooltip.prototype.getDefaults = function () {
1323 | return Tooltip.DEFAULTS
1324 | }
1325 |
1326 | Tooltip.prototype.getOptions = function (options) {
1327 | options = $.extend({}, this.getDefaults(), this.$element.data(), options)
1328 |
1329 | if (options.delay && typeof options.delay == 'number') {
1330 | options.delay = {
1331 | show: options.delay,
1332 | hide: options.delay
1333 | }
1334 | }
1335 |
1336 | return options
1337 | }
1338 |
1339 | Tooltip.prototype.getDelegateOptions = function () {
1340 | var options = {}
1341 | var defaults = this.getDefaults()
1342 |
1343 | this._options && $.each(this._options, function (key, value) {
1344 | if (defaults[key] != value) options[key] = value
1345 | })
1346 |
1347 | return options
1348 | }
1349 |
1350 | Tooltip.prototype.enter = function (obj) {
1351 | var self = obj instanceof this.constructor ?
1352 | obj : $(obj.currentTarget).data('bs.' + this.type)
1353 |
1354 | if (self && self.$tip && self.$tip.is(':visible')) {
1355 | self.hoverState = 'in'
1356 | return
1357 | }
1358 |
1359 | if (!self) {
1360 | self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
1361 | $(obj.currentTarget).data('bs.' + this.type, self)
1362 | }
1363 |
1364 | clearTimeout(self.timeout)
1365 |
1366 | self.hoverState = 'in'
1367 |
1368 | if (!self.options.delay || !self.options.delay.show) return self.show()
1369 |
1370 | self.timeout = setTimeout(function () {
1371 | if (self.hoverState == 'in') self.show()
1372 | }, self.options.delay.show)
1373 | }
1374 |
1375 | Tooltip.prototype.leave = function (obj) {
1376 | var self = obj instanceof this.constructor ?
1377 | obj : $(obj.currentTarget).data('bs.' + this.type)
1378 |
1379 | if (!self) {
1380 | self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
1381 | $(obj.currentTarget).data('bs.' + this.type, self)
1382 | }
1383 |
1384 | clearTimeout(self.timeout)
1385 |
1386 | self.hoverState = 'out'
1387 |
1388 | if (!self.options.delay || !self.options.delay.hide) return self.hide()
1389 |
1390 | self.timeout = setTimeout(function () {
1391 | if (self.hoverState == 'out') self.hide()
1392 | }, self.options.delay.hide)
1393 | }
1394 |
1395 | Tooltip.prototype.show = function () {
1396 | var e = $.Event('show.bs.' + this.type)
1397 |
1398 | if (this.hasContent() && this.enabled) {
1399 | this.$element.trigger(e)
1400 |
1401 | var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
1402 | if (e.isDefaultPrevented() || !inDom) return
1403 | var that = this
1404 |
1405 | var $tip = this.tip()
1406 |
1407 | var tipId = this.getUID(this.type)
1408 |
1409 | this.setContent()
1410 | $tip.attr('id', tipId)
1411 | this.$element.attr('aria-describedby', tipId)
1412 |
1413 | if (this.options.animation) $tip.addClass('fade')
1414 |
1415 | var placement = typeof this.options.placement == 'function' ?
1416 | this.options.placement.call(this, $tip[0], this.$element[0]) :
1417 | this.options.placement
1418 |
1419 | var autoToken = /\s?auto?\s?/i
1420 | var autoPlace = autoToken.test(placement)
1421 | if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
1422 |
1423 | $tip
1424 | .detach()
1425 | .css({ top: 0, left: 0, display: 'block' })
1426 | .addClass(placement)
1427 | .data('bs.' + this.type, this)
1428 |
1429 | this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
1430 |
1431 | var pos = this.getPosition()
1432 | var actualWidth = $tip[0].offsetWidth
1433 | var actualHeight = $tip[0].offsetHeight
1434 |
1435 | if (autoPlace) {
1436 | var orgPlacement = placement
1437 | var $container = this.options.container ? $(this.options.container) : this.$element.parent()
1438 | var containerDim = this.getPosition($container)
1439 |
1440 | placement = placement == 'bottom' && pos.bottom + actualHeight > containerDim.bottom ? 'top' :
1441 | placement == 'top' && pos.top - actualHeight < containerDim.top ? 'bottom' :
1442 | placement == 'right' && pos.right + actualWidth > containerDim.width ? 'left' :
1443 | placement == 'left' && pos.left - actualWidth < containerDim.left ? 'right' :
1444 | placement
1445 |
1446 | $tip
1447 | .removeClass(orgPlacement)
1448 | .addClass(placement)
1449 | }
1450 |
1451 | var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
1452 |
1453 | this.applyPlacement(calculatedOffset, placement)
1454 |
1455 | var complete = function () {
1456 | var prevHoverState = that.hoverState
1457 | that.$element.trigger('shown.bs.' + that.type)
1458 | that.hoverState = null
1459 |
1460 | if (prevHoverState == 'out') that.leave(that)
1461 | }
1462 |
1463 | $.support.transition && this.$tip.hasClass('fade') ?
1464 | $tip
1465 | .one('bsTransitionEnd', complete)
1466 | .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
1467 | complete()
1468 | }
1469 | }
1470 |
1471 | Tooltip.prototype.applyPlacement = function (offset, placement) {
1472 | var $tip = this.tip()
1473 | var width = $tip[0].offsetWidth
1474 | var height = $tip[0].offsetHeight
1475 |
1476 | // manually read margins because getBoundingClientRect includes difference
1477 | var marginTop = parseInt($tip.css('margin-top'), 10)
1478 | var marginLeft = parseInt($tip.css('margin-left'), 10)
1479 |
1480 | // we must check for NaN for ie 8/9
1481 | if (isNaN(marginTop)) marginTop = 0
1482 | if (isNaN(marginLeft)) marginLeft = 0
1483 |
1484 | offset.top = offset.top + marginTop
1485 | offset.left = offset.left + marginLeft
1486 |
1487 | // $.fn.offset doesn't round pixel values
1488 | // so we use setOffset directly with our own function B-0
1489 | $.offset.setOffset($tip[0], $.extend({
1490 | using: function (props) {
1491 | $tip.css({
1492 | top: Math.round(props.top),
1493 | left: Math.round(props.left)
1494 | })
1495 | }
1496 | }, offset), 0)
1497 |
1498 | $tip.addClass('in')
1499 |
1500 | // check to see if placing tip in new offset caused the tip to resize itself
1501 | var actualWidth = $tip[0].offsetWidth
1502 | var actualHeight = $tip[0].offsetHeight
1503 |
1504 | if (placement == 'top' && actualHeight != height) {
1505 | offset.top = offset.top + height - actualHeight
1506 | }
1507 |
1508 | var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
1509 |
1510 | if (delta.left) offset.left += delta.left
1511 | else offset.top += delta.top
1512 |
1513 | var isVertical = /top|bottom/.test(placement)
1514 | var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
1515 | var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'
1516 |
1517 | $tip.offset(offset)
1518 | this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
1519 | }
1520 |
1521 | Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
1522 | this.arrow()
1523 | .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
1524 | .css(isVertical ? 'top' : 'left', '')
1525 | }
1526 |
1527 | Tooltip.prototype.setContent = function () {
1528 | var $tip = this.tip()
1529 | var title = this.getTitle()
1530 |
1531 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
1532 | $tip.removeClass('fade in top bottom left right')
1533 | }
1534 |
1535 | Tooltip.prototype.hide = function (callback) {
1536 | var that = this
1537 | var $tip = $(this.$tip)
1538 | var e = $.Event('hide.bs.' + this.type)
1539 |
1540 | function complete() {
1541 | if (that.hoverState != 'in') $tip.detach()
1542 | that.$element
1543 | .removeAttr('aria-describedby')
1544 | .trigger('hidden.bs.' + that.type)
1545 | callback && callback()
1546 | }
1547 |
1548 | this.$element.trigger(e)
1549 |
1550 | if (e.isDefaultPrevented()) return
1551 |
1552 | $tip.removeClass('in')
1553 |
1554 | $.support.transition && $tip.hasClass('fade') ?
1555 | $tip
1556 | .one('bsTransitionEnd', complete)
1557 | .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
1558 | complete()
1559 |
1560 | this.hoverState = null
1561 |
1562 | return this
1563 | }
1564 |
1565 | Tooltip.prototype.fixTitle = function () {
1566 | var $e = this.$element
1567 | if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
1568 | $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
1569 | }
1570 | }
1571 |
1572 | Tooltip.prototype.hasContent = function () {
1573 | return this.getTitle()
1574 | }
1575 |
1576 | Tooltip.prototype.getPosition = function ($element) {
1577 | $element = $element || this.$element
1578 |
1579 | var el = $element[0]
1580 | var isBody = el.tagName == 'BODY'
1581 |
1582 | var elRect = el.getBoundingClientRect()
1583 | if (elRect.width == null) {
1584 | // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
1585 | elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
1586 | }
1587 | var elOffset = isBody ? { top: 0, left: 0 } : $element.offset()
1588 | var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
1589 | var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
1590 |
1591 | return $.extend({}, elRect, scroll, outerDims, elOffset)
1592 | }
1593 |
1594 | Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
1595 | return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
1596 | placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
1597 | placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
1598 | /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
1599 |
1600 | }
1601 |
1602 | Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
1603 | var delta = { top: 0, left: 0 }
1604 | if (!this.$viewport) return delta
1605 |
1606 | var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
1607 | var viewportDimensions = this.getPosition(this.$viewport)
1608 |
1609 | if (/right|left/.test(placement)) {
1610 | var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll
1611 | var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
1612 | if (topEdgeOffset < viewportDimensions.top) { // top overflow
1613 | delta.top = viewportDimensions.top - topEdgeOffset
1614 | } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
1615 | delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
1616 | }
1617 | } else {
1618 | var leftEdgeOffset = pos.left - viewportPadding
1619 | var rightEdgeOffset = pos.left + viewportPadding + actualWidth
1620 | if (leftEdgeOffset < viewportDimensions.left) { // left overflow
1621 | delta.left = viewportDimensions.left - leftEdgeOffset
1622 | } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow
1623 | delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
1624 | }
1625 | }
1626 |
1627 | return delta
1628 | }
1629 |
1630 | Tooltip.prototype.getTitle = function () {
1631 | var title
1632 | var $e = this.$element
1633 | var o = this.options
1634 |
1635 | title = $e.attr('data-original-title')
1636 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
1637 |
1638 | return title
1639 | }
1640 |
1641 | Tooltip.prototype.getUID = function (prefix) {
1642 | do prefix += ~~(Math.random() * 1000000)
1643 | while (document.getElementById(prefix))
1644 | return prefix
1645 | }
1646 |
1647 | Tooltip.prototype.tip = function () {
1648 | return (this.$tip = this.$tip || $(this.options.template))
1649 | }
1650 |
1651 | Tooltip.prototype.arrow = function () {
1652 | return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
1653 | }
1654 |
1655 | Tooltip.prototype.enable = function () {
1656 | this.enabled = true
1657 | }
1658 |
1659 | Tooltip.prototype.disable = function () {
1660 | this.enabled = false
1661 | }
1662 |
1663 | Tooltip.prototype.toggleEnabled = function () {
1664 | this.enabled = !this.enabled
1665 | }
1666 |
1667 | Tooltip.prototype.toggle = function (e) {
1668 | var self = this
1669 | if (e) {
1670 | self = $(e.currentTarget).data('bs.' + this.type)
1671 | if (!self) {
1672 | self = new this.constructor(e.currentTarget, this.getDelegateOptions())
1673 | $(e.currentTarget).data('bs.' + this.type, self)
1674 | }
1675 | }
1676 |
1677 | self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
1678 | }
1679 |
1680 | Tooltip.prototype.destroy = function () {
1681 | var that = this
1682 | clearTimeout(this.timeout)
1683 | this.hide(function () {
1684 | that.$element.off('.' + that.type).removeData('bs.' + that.type)
1685 | })
1686 | }
1687 |
1688 |
1689 | // TOOLTIP PLUGIN DEFINITION
1690 | // =========================
1691 |
1692 | function Plugin(option) {
1693 | return this.each(function () {
1694 | var $this = $(this)
1695 | var data = $this.data('bs.tooltip')
1696 | var options = typeof option == 'object' && option
1697 |
1698 | if (!data && /destroy|hide/.test(option)) return
1699 | if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
1700 | if (typeof option == 'string') data[option]()
1701 | })
1702 | }
1703 |
1704 | var old = $.fn.tooltip
1705 |
1706 | $.fn.tooltip = Plugin
1707 | $.fn.tooltip.Constructor = Tooltip
1708 |
1709 |
1710 | // TOOLTIP NO CONFLICT
1711 | // ===================
1712 |
1713 | $.fn.tooltip.noConflict = function () {
1714 | $.fn.tooltip = old
1715 | return this
1716 | }
1717 |
1718 | }(jQuery);
1719 |
1720 | /* ========================================================================
1721 | * Bootstrap: popover.js v3.3.4
1722 | * http://getbootstrap.com/javascript/#popovers
1723 | * ========================================================================
1724 | * Copyright 2011-2015 Twitter, Inc.
1725 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
1726 | * ======================================================================== */
1727 |
1728 |
1729 | +function ($) {
1730 | 'use strict';
1731 |
1732 | // POPOVER PUBLIC CLASS DEFINITION
1733 | // ===============================
1734 |
1735 | var Popover = function (element, options) {
1736 | this.init('popover', element, options)
1737 | }
1738 |
1739 | if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
1740 |
1741 | Popover.VERSION = '3.3.4'
1742 |
1743 | Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
1744 | placement: 'right',
1745 | trigger: 'click',
1746 | content: '',
1747 | template: ''
1748 | })
1749 |
1750 |
1751 | // NOTE: POPOVER EXTENDS tooltip.js
1752 | // ================================
1753 |
1754 | Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)
1755 |
1756 | Popover.prototype.constructor = Popover
1757 |
1758 | Popover.prototype.getDefaults = function () {
1759 | return Popover.DEFAULTS
1760 | }
1761 |
1762 | Popover.prototype.setContent = function () {
1763 | var $tip = this.tip()
1764 | var title = this.getTitle()
1765 | var content = this.getContent()
1766 |
1767 | $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
1768 | $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events
1769 | this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
1770 | ](content)
1771 |
1772 | $tip.removeClass('fade top bottom left right in')
1773 |
1774 | // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
1775 | // this manually by checking the contents.
1776 | if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
1777 | }
1778 |
1779 | Popover.prototype.hasContent = function () {
1780 | return this.getTitle() || this.getContent()
1781 | }
1782 |
1783 | Popover.prototype.getContent = function () {
1784 | var $e = this.$element
1785 | var o = this.options
1786 |
1787 | return $e.attr('data-content')
1788 | || (typeof o.content == 'function' ?
1789 | o.content.call($e[0]) :
1790 | o.content)
1791 | }
1792 |
1793 | Popover.prototype.arrow = function () {
1794 | return (this.$arrow = this.$arrow || this.tip().find('.arrow'))
1795 | }
1796 |
1797 |
1798 | // POPOVER PLUGIN DEFINITION
1799 | // =========================
1800 |
1801 | function Plugin(option) {
1802 | return this.each(function () {
1803 | var $this = $(this)
1804 | var data = $this.data('bs.popover')
1805 | var options = typeof option == 'object' && option
1806 |
1807 | if (!data && /destroy|hide/.test(option)) return
1808 | if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
1809 | if (typeof option == 'string') data[option]()
1810 | })
1811 | }
1812 |
1813 | var old = $.fn.popover
1814 |
1815 | $.fn.popover = Plugin
1816 | $.fn.popover.Constructor = Popover
1817 |
1818 |
1819 | // POPOVER NO CONFLICT
1820 | // ===================
1821 |
1822 | $.fn.popover.noConflict = function () {
1823 | $.fn.popover = old
1824 | return this
1825 | }
1826 |
1827 | }(jQuery);
1828 |
1829 | /* ========================================================================
1830 | * Bootstrap: scrollspy.js v3.3.4
1831 | * http://getbootstrap.com/javascript/#scrollspy
1832 | * ========================================================================
1833 | * Copyright 2011-2015 Twitter, Inc.
1834 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
1835 | * ======================================================================== */
1836 |
1837 |
1838 | +function ($) {
1839 | 'use strict';
1840 |
1841 | // SCROLLSPY CLASS DEFINITION
1842 | // ==========================
1843 |
1844 | function ScrollSpy(element, options) {
1845 | this.$body = $(document.body)
1846 | this.$scrollElement = $(element).is(document.body) ? $(window) : $(element)
1847 | this.options = $.extend({}, ScrollSpy.DEFAULTS, options)
1848 | this.selector = (this.options.target || '') + ' .nav li > a'
1849 | this.offsets = []
1850 | this.targets = []
1851 | this.activeTarget = null
1852 | this.scrollHeight = 0
1853 |
1854 | this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this))
1855 | this.refresh()
1856 | this.process()
1857 | }
1858 |
1859 | ScrollSpy.VERSION = '3.3.4'
1860 |
1861 | ScrollSpy.DEFAULTS = {
1862 | offset: 10
1863 | }
1864 |
1865 | ScrollSpy.prototype.getScrollHeight = function () {
1866 | return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight)
1867 | }
1868 |
1869 | ScrollSpy.prototype.refresh = function () {
1870 | var that = this
1871 | var offsetMethod = 'offset'
1872 | var offsetBase = 0
1873 |
1874 | this.offsets = []
1875 | this.targets = []
1876 | this.scrollHeight = this.getScrollHeight()
1877 |
1878 | if (!$.isWindow(this.$scrollElement[0])) {
1879 | offsetMethod = 'position'
1880 | offsetBase = this.$scrollElement.scrollTop()
1881 | }
1882 |
1883 | this.$body
1884 | .find(this.selector)
1885 | .map(function () {
1886 | var $el = $(this)
1887 | var href = $el.data('target') || $el.attr('href')
1888 | var $href = /^#./.test(href) && $(href)
1889 |
1890 | return ($href
1891 | && $href.length
1892 | && $href.is(':visible')
1893 | && [[$href[offsetMethod]().top + offsetBase, href]]) || null
1894 | })
1895 | .sort(function (a, b) { return a[0] - b[0] })
1896 | .each(function () {
1897 | that.offsets.push(this[0])
1898 | that.targets.push(this[1])
1899 | })
1900 | }
1901 |
1902 | ScrollSpy.prototype.process = function () {
1903 | var scrollTop = this.$scrollElement.scrollTop() + this.options.offset
1904 | var scrollHeight = this.getScrollHeight()
1905 | var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height()
1906 | var offsets = this.offsets
1907 | var targets = this.targets
1908 | var activeTarget = this.activeTarget
1909 | var i
1910 |
1911 | if (this.scrollHeight != scrollHeight) {
1912 | this.refresh()
1913 | }
1914 |
1915 | if (scrollTop >= maxScroll) {
1916 | return activeTarget != (i = targets[targets.length - 1]) && this.activate(i)
1917 | }
1918 |
1919 | if (activeTarget && scrollTop < offsets[0]) {
1920 | this.activeTarget = null
1921 | return this.clear()
1922 | }
1923 |
1924 | for (i = offsets.length; i--;) {
1925 | activeTarget != targets[i]
1926 | && scrollTop >= offsets[i]
1927 | && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1])
1928 | && this.activate(targets[i])
1929 | }
1930 | }
1931 |
1932 | ScrollSpy.prototype.activate = function (target) {
1933 | this.activeTarget = target
1934 |
1935 | this.clear()
1936 |
1937 | var selector = this.selector +
1938 | '[data-target="' + target + '"],' +
1939 | this.selector + '[href="' + target + '"]'
1940 |
1941 | var active = $(selector)
1942 | .parents('li')
1943 | .addClass('active')
1944 |
1945 | if (active.parent('.dropdown-menu').length) {
1946 | active = active
1947 | .closest('li.dropdown')
1948 | .addClass('active')
1949 | }
1950 |
1951 | active.trigger('activate.bs.scrollspy')
1952 | }
1953 |
1954 | ScrollSpy.prototype.clear = function () {
1955 | $(this.selector)
1956 | .parentsUntil(this.options.target, '.active')
1957 | .removeClass('active')
1958 | }
1959 |
1960 |
1961 | // SCROLLSPY PLUGIN DEFINITION
1962 | // ===========================
1963 |
1964 | function Plugin(option) {
1965 | return this.each(function () {
1966 | var $this = $(this)
1967 | var data = $this.data('bs.scrollspy')
1968 | var options = typeof option == 'object' && option
1969 |
1970 | if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options)))
1971 | if (typeof option == 'string') data[option]()
1972 | })
1973 | }
1974 |
1975 | var old = $.fn.scrollspy
1976 |
1977 | $.fn.scrollspy = Plugin
1978 | $.fn.scrollspy.Constructor = ScrollSpy
1979 |
1980 |
1981 | // SCROLLSPY NO CONFLICT
1982 | // =====================
1983 |
1984 | $.fn.scrollspy.noConflict = function () {
1985 | $.fn.scrollspy = old
1986 | return this
1987 | }
1988 |
1989 |
1990 | // SCROLLSPY DATA-API
1991 | // ==================
1992 |
1993 | $(window).on('load.bs.scrollspy.data-api', function () {
1994 | $('[data-spy="scroll"]').each(function () {
1995 | var $spy = $(this)
1996 | Plugin.call($spy, $spy.data())
1997 | })
1998 | })
1999 |
2000 | }(jQuery);
2001 |
2002 | /* ========================================================================
2003 | * Bootstrap: tab.js v3.3.4
2004 | * http://getbootstrap.com/javascript/#tabs
2005 | * ========================================================================
2006 | * Copyright 2011-2015 Twitter, Inc.
2007 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
2008 | * ======================================================================== */
2009 |
2010 |
2011 | +function ($) {
2012 | 'use strict';
2013 |
2014 | // TAB CLASS DEFINITION
2015 | // ====================
2016 |
2017 | var Tab = function (element) {
2018 | this.element = $(element)
2019 | }
2020 |
2021 | Tab.VERSION = '3.3.4'
2022 |
2023 | Tab.TRANSITION_DURATION = 150
2024 |
2025 | Tab.prototype.show = function () {
2026 | var $this = this.element
2027 | var $ul = $this.closest('ul:not(.dropdown-menu)')
2028 | var selector = $this.data('target')
2029 |
2030 | if (!selector) {
2031 | selector = $this.attr('href')
2032 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
2033 | }
2034 |
2035 | if ($this.parent('li').hasClass('active')) return
2036 |
2037 | var $previous = $ul.find('.active:last a')
2038 | var hideEvent = $.Event('hide.bs.tab', {
2039 | relatedTarget: $this[0]
2040 | })
2041 | var showEvent = $.Event('show.bs.tab', {
2042 | relatedTarget: $previous[0]
2043 | })
2044 |
2045 | $previous.trigger(hideEvent)
2046 | $this.trigger(showEvent)
2047 |
2048 | if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return
2049 |
2050 | var $target = $(selector)
2051 |
2052 | this.activate($this.closest('li'), $ul)
2053 | this.activate($target, $target.parent(), function () {
2054 | $previous.trigger({
2055 | type: 'hidden.bs.tab',
2056 | relatedTarget: $this[0]
2057 | })
2058 | $this.trigger({
2059 | type: 'shown.bs.tab',
2060 | relatedTarget: $previous[0]
2061 | })
2062 | })
2063 | }
2064 |
2065 | Tab.prototype.activate = function (element, container, callback) {
2066 | var $active = container.find('> .active')
2067 | var transition = callback
2068 | && $.support.transition
2069 | && (($active.length && $active.hasClass('fade')) || !!container.find('> .fade').length)
2070 |
2071 | function next() {
2072 | $active
2073 | .removeClass('active')
2074 | .find('> .dropdown-menu > .active')
2075 | .removeClass('active')
2076 | .end()
2077 | .find('[data-toggle="tab"]')
2078 | .attr('aria-expanded', false)
2079 |
2080 | element
2081 | .addClass('active')
2082 | .find('[data-toggle="tab"]')
2083 | .attr('aria-expanded', true)
2084 |
2085 | if (transition) {
2086 | element[0].offsetWidth // reflow for transition
2087 | element.addClass('in')
2088 | } else {
2089 | element.removeClass('fade')
2090 | }
2091 |
2092 | if (element.parent('.dropdown-menu').length) {
2093 | element
2094 | .closest('li.dropdown')
2095 | .addClass('active')
2096 | .end()
2097 | .find('[data-toggle="tab"]')
2098 | .attr('aria-expanded', true)
2099 | }
2100 |
2101 | callback && callback()
2102 | }
2103 |
2104 | $active.length && transition ?
2105 | $active
2106 | .one('bsTransitionEnd', next)
2107 | .emulateTransitionEnd(Tab.TRANSITION_DURATION) :
2108 | next()
2109 |
2110 | $active.removeClass('in')
2111 | }
2112 |
2113 |
2114 | // TAB PLUGIN DEFINITION
2115 | // =====================
2116 |
2117 | function Plugin(option) {
2118 | return this.each(function () {
2119 | var $this = $(this)
2120 | var data = $this.data('bs.tab')
2121 |
2122 | if (!data) $this.data('bs.tab', (data = new Tab(this)))
2123 | if (typeof option == 'string') data[option]()
2124 | })
2125 | }
2126 |
2127 | var old = $.fn.tab
2128 |
2129 | $.fn.tab = Plugin
2130 | $.fn.tab.Constructor = Tab
2131 |
2132 |
2133 | // TAB NO CONFLICT
2134 | // ===============
2135 |
2136 | $.fn.tab.noConflict = function () {
2137 | $.fn.tab = old
2138 | return this
2139 | }
2140 |
2141 |
2142 | // TAB DATA-API
2143 | // ============
2144 |
2145 | var clickHandler = function (e) {
2146 | e.preventDefault()
2147 | Plugin.call($(this), 'show')
2148 | }
2149 |
2150 | $(document)
2151 | .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler)
2152 | .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler)
2153 |
2154 | }(jQuery);
2155 |
2156 | /* ========================================================================
2157 | * Bootstrap: affix.js v3.3.4
2158 | * http://getbootstrap.com/javascript/#affix
2159 | * ========================================================================
2160 | * Copyright 2011-2015 Twitter, Inc.
2161 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
2162 | * ======================================================================== */
2163 |
2164 |
2165 | +function ($) {
2166 | 'use strict';
2167 |
2168 | // AFFIX CLASS DEFINITION
2169 | // ======================
2170 |
2171 | var Affix = function (element, options) {
2172 | this.options = $.extend({}, Affix.DEFAULTS, options)
2173 |
2174 | this.$target = $(this.options.target)
2175 | .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
2176 | .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this))
2177 |
2178 | this.$element = $(element)
2179 | this.affixed = null
2180 | this.unpin = null
2181 | this.pinnedOffset = null
2182 |
2183 | this.checkPosition()
2184 | }
2185 |
2186 | Affix.VERSION = '3.3.4'
2187 |
2188 | Affix.RESET = 'affix affix-top affix-bottom'
2189 |
2190 | Affix.DEFAULTS = {
2191 | offset: 0,
2192 | target: window
2193 | }
2194 |
2195 | Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) {
2196 | var scrollTop = this.$target.scrollTop()
2197 | var position = this.$element.offset()
2198 | var targetHeight = this.$target.height()
2199 |
2200 | if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false
2201 |
2202 | if (this.affixed == 'bottom') {
2203 | if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom'
2204 | return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom'
2205 | }
2206 |
2207 | var initializing = this.affixed == null
2208 | var colliderTop = initializing ? scrollTop : position.top
2209 | var colliderHeight = initializing ? targetHeight : height
2210 |
2211 | if (offsetTop != null && scrollTop <= offsetTop) return 'top'
2212 | if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom'
2213 |
2214 | return false
2215 | }
2216 |
2217 | Affix.prototype.getPinnedOffset = function () {
2218 | if (this.pinnedOffset) return this.pinnedOffset
2219 | this.$element.removeClass(Affix.RESET).addClass('affix')
2220 | var scrollTop = this.$target.scrollTop()
2221 | var position = this.$element.offset()
2222 | return (this.pinnedOffset = position.top - scrollTop)
2223 | }
2224 |
2225 | Affix.prototype.checkPositionWithEventLoop = function () {
2226 | setTimeout($.proxy(this.checkPosition, this), 1)
2227 | }
2228 |
2229 | Affix.prototype.checkPosition = function () {
2230 | if (!this.$element.is(':visible')) return
2231 |
2232 | var height = this.$element.height()
2233 | var offset = this.options.offset
2234 | var offsetTop = offset.top
2235 | var offsetBottom = offset.bottom
2236 | var scrollHeight = $(document.body).height()
2237 |
2238 | if (typeof offset != 'object') offsetBottom = offsetTop = offset
2239 | if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element)
2240 | if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element)
2241 |
2242 | var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom)
2243 |
2244 | if (this.affixed != affix) {
2245 | if (this.unpin != null) this.$element.css('top', '')
2246 |
2247 | var affixType = 'affix' + (affix ? '-' + affix : '')
2248 | var e = $.Event(affixType + '.bs.affix')
2249 |
2250 | this.$element.trigger(e)
2251 |
2252 | if (e.isDefaultPrevented()) return
2253 |
2254 | this.affixed = affix
2255 | this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null
2256 |
2257 | this.$element
2258 | .removeClass(Affix.RESET)
2259 | .addClass(affixType)
2260 | .trigger(affixType.replace('affix', 'affixed') + '.bs.affix')
2261 | }
2262 |
2263 | if (affix == 'bottom') {
2264 | this.$element.offset({
2265 | top: scrollHeight - height - offsetBottom
2266 | })
2267 | }
2268 | }
2269 |
2270 |
2271 | // AFFIX PLUGIN DEFINITION
2272 | // =======================
2273 |
2274 | function Plugin(option) {
2275 | return this.each(function () {
2276 | var $this = $(this)
2277 | var data = $this.data('bs.affix')
2278 | var options = typeof option == 'object' && option
2279 |
2280 | if (!data) $this.data('bs.affix', (data = new Affix(this, options)))
2281 | if (typeof option == 'string') data[option]()
2282 | })
2283 | }
2284 |
2285 | var old = $.fn.affix
2286 |
2287 | $.fn.affix = Plugin
2288 | $.fn.affix.Constructor = Affix
2289 |
2290 |
2291 | // AFFIX NO CONFLICT
2292 | // =================
2293 |
2294 | $.fn.affix.noConflict = function () {
2295 | $.fn.affix = old
2296 | return this
2297 | }
2298 |
2299 |
2300 | // AFFIX DATA-API
2301 | // ==============
2302 |
2303 | $(window).on('load', function () {
2304 | $('[data-spy="affix"]').each(function () {
2305 | var $spy = $(this)
2306 | var data = $spy.data()
2307 |
2308 | data.offset = data.offset || {}
2309 |
2310 | if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom
2311 | if (data.offsetTop != null) data.offset.top = data.offsetTop
2312 |
2313 | Plugin.call($spy, data)
2314 | })
2315 | })
2316 |
2317 | }(jQuery);
2318 |
--------------------------------------------------------------------------------
/app/static/js/bootstrap.min.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 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.4",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.4",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.4",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.4",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.4",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(b){if(/(38|40|27|32)/.test(b.which)&&!/input|textarea/i.test(b.target.tagName)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var e=c(d),g=e.hasClass("open");if(!g&&27!=b.which||g&&27==b.which)return 27==b.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find('[role="menu"]'+h+', [role="listbox"]'+h);if(i.length){var j=i.index(b.target);38==b.which&&j>0&&j--,40==b.which&&j').appendTo(this.$body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){return this.ignoreBackdropClick?void(this.ignoreBackdropClick=!1):void(a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus():this.hide()))},this)),f&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;f?this.$backdrop.one("bsTransitionEnd",b).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):b()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var g=function(){d.removeBackdrop(),b&&b()};a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",g).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):g()}else b&&b()},c.prototype.handleUpdate=function(){this.adjustDialog()},c.prototype.adjustDialog=function(){var a=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport),this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c&&c.$tip&&c.$tip.is(":visible")?void(c.hoverState="in"):(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.options.container?a(this.options.container):this.$element.parent(),p=this.getPosition(o);h="bottom"==h&&k.bottom+m>p.bottom?"top":"top"==h&&k.top-mp.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type)})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.4",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.4",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.4",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=a(document.body).height();"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery);
--------------------------------------------------------------------------------
/app/templates/extended.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "layout.html" %}
3 | {% block bokeh %}
4 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/app/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "layout.html" %}
3 | {% block bokeh %}
4 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/app/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | bitpredict.io
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
62 |
63 |
64 |
65 |
66 |
67 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | {% block bokeh %}{% endblock %}
81 |
82 |
83 |
84 |
Implementation
85 |
At one second intervals, a trained model makes a 30 second price forecast using live order and trade data from the Bitfinex exchange.
86 |
Simulated trades are executed at a one basis point threshold and held for 30 seconds, with only one open position allowed at a time.
87 |
Theoretical returns are calculated purely as measure of predictive performance. Most of the price movements are too small to be profitable in practice. Transaction costs are not considered.
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/app/templates/performance.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "layout.html" %}
3 | {% block bokeh %}
4 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/collect-data/collect_books.py:
--------------------------------------------------------------------------------
1 | import urllib2
2 | import time
3 | import json
4 | from pymongo import MongoClient
5 | import sys
6 |
7 | api = 'https://api.bitfinex.com/v1'
8 | symbol = sys.argv[1]
9 | limit = 25
10 | book_url = '{0}/book/{1}usd?limit_bids={2}&limit_asks={2}'\
11 | .format(api, symbol, limit)
12 |
13 | client = MongoClient()
14 | db = client['bitmicro']
15 | ltc_books = db[symbol+'_books']
16 |
17 |
18 | def format_book_entry(entry):
19 | '''
20 | Converts book data to float
21 | '''
22 | if all(key in entry for key in ('amount', 'price', 'timestamp')):
23 | entry['amount'] = float(entry['amount'])
24 | entry['price'] = float(entry['price'])
25 | entry['timestamp'] = float(entry['timestamp'])
26 | return entry
27 |
28 |
29 | def get_json(url):
30 | '''
31 | Gets json from the API
32 | '''
33 | resp = urllib2.urlopen(url)
34 | return json.load(resp, object_hook=format_book_entry), resp.getcode()
35 |
36 |
37 | print 'Running...'
38 | while True:
39 | start = time.time()
40 | try:
41 | book, code = get_json(book_url)
42 | except Exception as e:
43 | print e
44 | sys.exc_clear()
45 | else:
46 | if code != 200:
47 | print code
48 | else:
49 | book['_id'] = time.time()
50 | ltc_books.insert_one(book)
51 | time_delta = time.time()-start
52 | if time_delta < 1.0:
53 | time.sleep(1-time_delta)
54 |
--------------------------------------------------------------------------------
/collect-data/collect_trades.py:
--------------------------------------------------------------------------------
1 | import urllib2
2 | import time
3 | import json
4 | from pymongo import MongoClient
5 | import sys
6 |
7 | api = 'https://api.bitfinex.com/v1'
8 | symbol = sys.argv[1]
9 | limit = 1000
10 |
11 | client = MongoClient()
12 | db = client['bitmicro']
13 | ltc_trades = db[symbol+'_trades']
14 |
15 |
16 | def format_trade(trade):
17 | '''
18 | Formats trade data
19 | '''
20 | if all(key in trade for key in ('tid', 'amount', 'price', 'timestamp')):
21 | trade['_id'] = trade.pop('tid')
22 | trade['amount'] = float(trade['amount'])
23 | trade['price'] = float(trade['price'])
24 | trade['timestamp'] = float(trade['timestamp'])
25 |
26 | return trade
27 |
28 |
29 | def get_json(url):
30 | '''
31 | Gets json from the API
32 | '''
33 | resp = urllib2.urlopen(url)
34 | return json.load(resp, object_hook=format_trade), resp.getcode()
35 |
36 |
37 | print 'Running...'
38 | last_timestamp = 0
39 | while True:
40 | start = time.time()
41 | url = '{0}/trades/{1}usd?timestamp={2}&limit_trades={3}'\
42 | .format(api, symbol, last_timestamp, limit)
43 | try:
44 | trades, code = get_json(url)
45 | except Exception as e:
46 | print e
47 | sys.exc_clear()
48 | else:
49 | if code != 200:
50 | print code
51 | else:
52 | for trade in trades:
53 | ltc_trades.update_one({'_id': trade['_id']},
54 | {'$setOnInsert': trade}, upsert=True)
55 | last_timestamp = trades[0]['timestamp'] - 5
56 | time_delta = time.time()-start
57 | if time_delta < 1.0:
58 | time.sleep(1-time_delta)
59 |
--------------------------------------------------------------------------------
/images/live_results.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/images/live_results.png
--------------------------------------------------------------------------------
/images/strategy01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/images/strategy01.png
--------------------------------------------------------------------------------
/images/strategy05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/images/strategy05.png
--------------------------------------------------------------------------------
/images/strategy10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/images/strategy10.png
--------------------------------------------------------------------------------
/model/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/model/__init__.py
--------------------------------------------------------------------------------
/model/features.py:
--------------------------------------------------------------------------------
1 | import pymongo
2 | import pandas as pd
3 | from math import log
4 | from time import time
5 | import sys
6 | from scipy.stats import linregress
7 | import pickle
8 |
9 | client = pymongo.MongoClient()
10 | db = client['bitmicro']
11 |
12 |
13 | def get_book_df(symbol, limit, convert_timestamps=False):
14 | '''
15 | Returns a DataFrame of book data
16 | '''
17 | books_db = db[symbol+'_books']
18 | cursor = books_db.find().sort('_id', -1).limit(limit)
19 | books = pd.DataFrame(list(cursor))
20 | books = books.set_index('_id')
21 | if convert_timestamps:
22 | books.index = pd.to_datetime(books.index, unit='s')
23 |
24 | def to_df(x):
25 | return pd.DataFrame(x[:10])
26 | return books.applymap(to_df).sort_index()
27 |
28 |
29 | def get_width_and_mid(books):
30 | '''
31 | Returns width of best market and midpoint for each data point in DataFrame
32 | of book data
33 | '''
34 | best_bid = books.bids.apply(lambda x: x.price[0])
35 | best_ask = books.asks.apply(lambda x: x.price[0])
36 | return best_ask-best_bid, (best_bid + best_ask)/2
37 |
38 |
39 | def get_future_mid(books, offset, sensitivity):
40 | '''
41 | Returns percent change of future midpoints for each data point in DataFrame
42 | of book data
43 | '''
44 |
45 | def future(timestamp):
46 | i = books.index.get_loc(timestamp+offset, method='nearest')
47 | if abs(books.index[i] - (timestamp+offset)) < sensitivity:
48 | return books.mid.iloc[i]
49 | return (books.index.map(future)/books.mid).apply(log)
50 |
51 |
52 | def get_power_imbalance(books, n=10, power=2):
53 | '''
54 | Returns a measure of the imbalance between bids and offers for each data
55 | point in DataFrame of book data
56 | '''
57 |
58 | def calc_imbalance(book):
59 | def calc(x):
60 | return x.amount*(.5*book.width/(x.price-book.mid))**power
61 | bid_imbalance = book.bids.iloc[:n].apply(calc, axis=1)
62 | ask_imbalance = book.asks.iloc[:n].apply(calc, axis=1)
63 | return (bid_imbalance-ask_imbalance).sum()
64 | imbalance = books.apply(calc_imbalance, axis=1)
65 | return imbalance
66 |
67 |
68 | def get_power_adjusted_price(books, n=10, power=2):
69 | '''
70 | Returns the percent change of an average of order prices weighted by inverse
71 | distance-wieghted volume for each data point in DataFrame of book data
72 | '''
73 |
74 | def calc_adjusted_price(book):
75 | def calc(x):
76 | return x.amount*(.5*book.width/(x.price-book.mid))**power
77 | bid_inv = 1/book.bids.iloc[:n].apply(calc, axis=1)
78 | ask_inv = 1/book.asks.iloc[:n].apply(calc, axis=1)
79 | bid_price = book.bids.price.iloc[:n]
80 | ask_price = book.asks.price.iloc[:n]
81 | return (bid_price*bid_inv + ask_price*ask_inv).sum() /\
82 | (bid_inv + ask_inv).sum()
83 | adjusted = books.apply(calc_adjusted_price, axis=1)
84 | return (adjusted/books.mid).apply(log).fillna(0)
85 |
86 |
87 | def get_trade_df(symbol, min_ts, max_ts, convert_timestamps=False):
88 | '''
89 | Returns a DataFrame of trades for symbol in time range
90 | '''
91 | trades_db = db[symbol+'_trades']
92 | query = {'timestamp': {'$gt': min_ts, '$lt': max_ts}}
93 | cursor = trades_db.find(query).sort('_id', pymongo.ASCENDING)
94 | trades = pd.DataFrame(list(cursor))
95 | if not trades.empty:
96 | trades = trades.set_index('_id')
97 | if convert_timestamps:
98 | trades.index = pd.to_datetime(trades.index, unit='s')
99 | return trades
100 |
101 |
102 | def get_trades_indexes(books, trades, offset, live=False):
103 | '''
104 | Returns indexes of trades in offset range for each data point in DataFrame
105 | of book data
106 | '''
107 | def indexes(ts):
108 | ts = int(ts)
109 | i_0 = trades.timestamp.searchsorted([ts-offset], side='left')[0]
110 | if live:
111 | i_n = -1
112 | else:
113 | i_n = trades.timestamp.searchsorted([ts-1], side='right')[0]
114 | return (i_0, i_n)
115 | return books.index.map(indexes)
116 |
117 |
118 | def get_trades_count(books, trades):
119 | '''
120 | Returns a count of trades for each data point in DataFrame of book data
121 | '''
122 | def count(x):
123 | return len(trades.iloc[x.indexes[0]:x.indexes[1]])
124 | return books.apply(count, axis=1)
125 |
126 |
127 | def get_trades_average(books, trades):
128 | '''
129 | Returns the percent change of a volume-weighted average of trades for each
130 | data point in DataFrame of book data
131 | '''
132 |
133 | def mean_trades(x):
134 | trades_n = trades.iloc[x.indexes[0]:x.indexes[1]]
135 | if not trades_n.empty:
136 | return (trades_n.price*trades_n.amount).sum()/trades_n.amount.sum()
137 | return (books.mid/books.apply(mean_trades, axis=1)).apply(log).fillna(0)
138 |
139 |
140 | def get_aggressor(books, trades):
141 | '''
142 | Returns a measure of whether trade aggressors were buyers or sellers for
143 | each data point in DataFrame of book data
144 | '''
145 |
146 | def aggressor(x):
147 | trades_n = trades.iloc[x.indexes[0]:x.indexes[1]]
148 | if trades_n.empty:
149 | return 0
150 | buys = trades_n['type'] == 'buy'
151 | buy_vol = trades_n[buys].amount.sum()
152 | sell_vol = trades_n[~buys].amount.sum()
153 | return buy_vol - sell_vol
154 | return books.apply(aggressor, axis=1)
155 |
156 |
157 | def get_trend(books, trades):
158 | '''
159 | Returns the linear trend in previous trades for each data point in DataFrame
160 | of book data
161 | '''
162 |
163 | def trend(x):
164 | trades_n = trades.iloc[x.indexes[0]:x.indexes[1]]
165 | if len(trades_n) < 3:
166 | return 0
167 | else:
168 | return linregress(trades_n.index.values, trades_n.price.values)[0]
169 | return books.apply(trend, axis=1)
170 |
171 |
172 | def check_times(books):
173 | '''
174 | Returns list of differences between collection time and max book timestamps
175 | for verification purposes
176 | '''
177 | time_diff = []
178 | for i in range(len(books)):
179 | book = books.iloc[i]
180 | ask_ts = max(book.asks.timestamp)
181 | bid_ts = max(book.bids.timestamp)
182 | ts = max(ask_ts, bid_ts)
183 | time_diff.append(book.name-ts)
184 | return time_diff
185 |
186 |
187 | def make_features(symbol, sample, mid_offsets,
188 | trades_offsets, powers, live=False):
189 | '''
190 | Returns a DataFrame with targets and features
191 | '''
192 | start = time()
193 | stage = time()
194 |
195 | # Book related features:
196 | books = get_book_df(symbol, sample)
197 | if not live:
198 | print 'get book data run time:', (time()-stage)/60, 'minutes'
199 | stage = time()
200 | books['width'], books['mid'] = get_width_and_mid(books)
201 | if not live:
202 | print 'width and mid run time:', (time()-stage)/60, 'minutes'
203 | stage = time()
204 | for n in mid_offsets:
205 | books['mid{}'.format(n)] = get_future_mid(books, n, sensitivity=1)
206 | if not live:
207 | books = books.dropna()
208 | print 'offset mids run time:', (time()-stage)/60, 'minutes'
209 | stage = time()
210 | for p in powers:
211 | books['imbalance{}'.format(p)] = get_power_imbalance(books, 10, p)
212 | books['adj_price{}'.format(p)] = get_power_adjusted_price(books, 10, p)
213 | if not live:
214 | print 'power calcs run time:', (time()-stage)/60, 'minutes'
215 | stage = time()
216 | books = books.drop(['bids', 'asks'], axis=1)
217 |
218 | # Trade related features:
219 | min_ts = books.index.min() - trades_offsets[-1]
220 | max_ts = books.index.max()
221 | if live:
222 | max_ts += 10
223 | trades = get_trade_df(symbol, min_ts, max_ts)
224 | for n in trades_offsets:
225 | if trades.empty:
226 | books['indexes'] = 0
227 | books['t{}_count'.format(n)] = 0
228 | books['t{}_av'.format(n)] = 0
229 | books['agg{}'.format(n)] = 0
230 | books['trend{}'.format(n)] = 0
231 | else:
232 | books['indexes'] = get_trades_indexes(books, trades, n, live)
233 | books['t{}_count'.format(n)] = get_trades_count(books, trades)
234 | books['t{}_av'.format(n)] = get_trades_average(books, trades)
235 | books['agg{}'.format(n)] = get_aggressor(books, trades)
236 | books['trend{}'.format(n)] = get_trend(books, trades)
237 | if not live:
238 | print 'trade features run time:', (time()-stage)/60, 'minutes'
239 | stage = time()
240 | print 'make_features run time:', (time()-start)/60, 'minutes'
241 |
242 | return books.drop('indexes', axis=1)
243 |
244 |
245 | def make_data(symbol, sample):
246 | '''
247 | Convenience function for calling make_features
248 | '''
249 | data = make_features(symbol,
250 | sample=sample,
251 | mid_offsets=[30],
252 | trades_offsets=[30, 60, 120, 180],
253 | powers=[2, 4, 8])
254 | return data
255 |
256 | if __name__ == '__main__' and len(sys.argv) == 4:
257 | data = make_data(sys.argv[1], int(sys.argv[2]))
258 | with open(sys.argv[3], 'w+') as f:
259 | pickle.dump(data, f)
260 |
--------------------------------------------------------------------------------
/model/model.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from sklearn.ensemble import RandomForestRegressor
3 | from sklearn.ensemble import GradientBoostingRegressor
4 | import pickle
5 | import pandas as pd
6 |
7 |
8 | def cross_validate(X, y, model, window):
9 | '''
10 | Cross validates time series data using a shifting window where train data is
11 | always before test data
12 | '''
13 | in_sample_score = []
14 | out_sample_score = []
15 | for i in range(1, len(y)/window):
16 | train_index = np.arange(0, i*window)
17 | test_index = np.arange(i*window, (i+1)*window)
18 | y_train = y.take(train_index)
19 | y_test = y.take(test_index)
20 | X_train = X.take(train_index, axis=0)
21 | X_test = X.take(test_index, axis=0)
22 | model.fit(X_train, y_train)
23 | in_sample_score.append(model.score(X_train, y_train))
24 | out_sample_score.append(model.score(X_test, y_test))
25 | print 'Window', i
26 | print 'in-sample score', in_sample_score[-1]
27 | print 'out-sample score:', out_sample_score[-1]
28 | print '---'
29 | return model, np.mean(in_sample_score), np.mean(out_sample_score)
30 |
31 |
32 | def fit_forest(X, y, window=100000, estimators=100,
33 | samples_leaf=250, validate=True):
34 | '''
35 | Fits Random Forest
36 | '''
37 | model = RandomForestRegressor(n_estimators=estimators,
38 | min_samples_leaf=samples_leaf,
39 | random_state=42,
40 | n_jobs=-1)
41 | if validate:
42 | return cross_validate(X, y, model, window)
43 | return model.fit(X, y)
44 |
45 |
46 | def fit_boosting(X, y, window=100000, estimators=250, learning=.01,
47 | samples_leaf=500, depth=20, validate=False):
48 | '''
49 | Fits Gradient Boosting
50 | '''
51 | model = GradientBoostingRegressor(n_estimators=estimators,
52 | learning_rate=learning,
53 | min_samples_leaf=samples_leaf,
54 | max_depth=depth,
55 | random_state=42)
56 | if validate:
57 | return cross_validate(X, y, model, window)
58 | return model.fit(X, y)
59 |
60 |
61 | def grid_search(X, y, split, learn=[.01], samples_leaf=[250, 350, 500],
62 | depth=[10, 15]):
63 | '''
64 | Runs a grid search for GBM on split data
65 | '''
66 | for l in learn:
67 | for s in samples_leaf:
68 | for d in depth:
69 | model = GradientBoostingRegressor(n_estimators=250,
70 | learning_rate=l,
71 | min_samples_leaf=s,
72 | max_depth=d,
73 | random_state=42)
74 | model.fit(X.values[:split], y.values[:split])
75 | in_score = model.score(X.values[:split], y.values[:split])
76 | out_score = model.score(X.values[split:], y.values[split:])
77 | print 'learning_rate: {}, min_samples_leaf: {}, max_depth: {}'.\
78 | format(l, s, d)
79 | print 'in-sample score:', in_score
80 | print 'out-sample score:', out_score
81 | print ''
82 |
83 |
84 | def run_models(data, window, model_function, drop_zeros=False):
85 | '''
86 | Runs cross-validated models with a range of target offsets and outputs
87 | results sorted by out-of-sample performance
88 | '''
89 | mids = [col for col in data.columns if 'mid' in col]
90 | prevs = [col for col in data.columns if 'prev' in col]
91 | in_reg_scores = {}
92 | out_reg_scores = {}
93 | for i in range(len(mids)):
94 | print 'fitting model #{}...'.format(i+1)
95 | m = mids[i]
96 | p = prevs[i]
97 | if drop_zeros:
98 | y = data[data[m] != 0][m].values
99 | prev = data[data[m] != 0][p]
100 | X = data[data[m] != 0].drop(mids+prevs, axis=1)
101 | X = X.join(prev)
102 | X = X.values
103 | else:
104 | y = data[m].values
105 | prev = data[p]
106 | X = data.drop(mids+prevs, axis=1)
107 | X = X.join(prev)
108 | X = X.values
109 |
110 | _, in_reg_score, out_reg_score = model_function(X, y, window)
111 | in_reg_scores[m] = in_reg_score
112 | out_reg_scores[out_reg_score] = m
113 |
114 | print '\nrandom forest regressor r^2:'
115 | for score in sorted(out_reg_scores):
116 | m = out_reg_scores[score]
117 | print 'out-sample', m, score
118 | print 'in-sample', m, in_reg_scores[m], '\n'
119 |
120 |
121 | def get_feature_importances(fitted_model, labels):
122 | '''
123 | Returns labels sorted by feature importance
124 | '''
125 | labels = np.array(labels)
126 | importances = fitted_model.feature_importances_
127 | indexes = np.argsort(importances)[::-1]
128 | for i in indexes:
129 | print '{}: {}'.format(labels[i], importances[i])
130 | return labels[indexes]
131 |
132 |
133 | def get_pickle(filename):
134 | '''
135 | Pickle convenience function
136 | '''
137 | with open(filename, 'r') as f:
138 | data = pickle.load(f)
139 | return data
140 |
141 |
142 | def append_data(df1, df2):
143 | '''
144 | Append df2 to df1
145 | '''
146 | df = pd.concat((df1, df2))
147 | return df.groupby(df.index).first()
148 |
--------------------------------------------------------------------------------
/model/strategy.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 | from sklearn.ensemble import RandomForestRegressor
3 | import numpy as np
4 | import matplotlib.ticker as mtick
5 |
6 |
7 | def fit_and_trade(data, cols, split, threshold):
8 | '''
9 | Fits and backtests a theoretical trading strategy
10 | '''
11 | data = data[data.width > 0]
12 | X = data[cols]
13 | y = data.mid30
14 | X_train = X.iloc[:split]
15 | X_test = X.iloc[split:]
16 | y_train = y.iloc[:split]
17 | y_test = y.iloc[split:]
18 | regressor = RandomForestRegressor(n_estimators=100,
19 | min_samples_leaf=500,
20 | random_state=42,
21 | n_jobs=-1)
22 | regressor.fit(X_train.values, y_train.values)
23 | trade(X_test.values, y_test.values, regressor, threshold)
24 |
25 |
26 | def trade(X, y, index, model, threshold):
27 | '''
28 | Backtests a theoretical trading strategy
29 | '''
30 | print 'r-squared', model.score(X, y)
31 | preds = model.predict(X)
32 | trades = np.zeros(len(preds))
33 | count = 0
34 | active = False
35 | for i, pred in enumerate(preds):
36 | if active:
37 | count += 1
38 | if count == 30:
39 | count = 0
40 | active = False
41 | elif abs(pred) > threshold:
42 | active = True
43 | trades[i] = np.sign(pred)
44 |
45 | returns = trades*y*100
46 | trades_only = returns[trades != 0]
47 | mean_return = trades_only.mean()
48 | accuracy = sum(trades_only > 0)*1./len(trades_only)
49 | profit = np.cumsum(returns)
50 | plt.figure(dpi=100000)
51 | fig, ax = plt.subplots()
52 | plt.plot(index, profit)
53 | plt.title('Trading at Every {}% Prediction (No Transaction Costs)'
54 | .format(threshold*100))
55 | plt.ylabel('Returns')
56 | plt.xticks(rotation=45)
57 | formatter = mtick.FormatStrFormatter('%.0f%%')
58 | ax.yaxis.set_major_formatter(formatter)
59 | return_text = 'Average Return: {:.4f} %'.format(mean_return)
60 | trades_text = 'Total Trades: {:d}'.format(len(trades_only))
61 | accuracy_text = 'Accuracy: {:.2f} %'.format(accuracy*100)
62 | plt.text(.05, .85, return_text, transform=ax.transAxes)
63 | plt.text(.05, .78, trades_text, transform=ax.transAxes)
64 | plt.text(.05, .71, accuracy_text, transform=ax.transAxes)
65 | plt.show()
66 |
--------------------------------------------------------------------------------
/mongo-queries/group_ltc.js:
--------------------------------------------------------------------------------
1 | // group ltc trades by second, with volume-weigted price
2 | db.ltc_trades.aggregate([
3 | { $match: { timestamp: { $gt: 1440117467} } },
4 | { $group: {
5 | _id: "$timestamp",
6 | total_price: { $sum: { $multiply: [ "$price", "$amount" ] } },
7 | total_quantity: { $sum: "$amount" }
8 | } },
9 | { $project: {
10 | price: { $divide: [ "$total_price", "$total_quantity" ] },
11 | amount: "$total_quantity"
12 | } },
13 | { $sort: {_id: 1} },
14 | { $out: "grouped_ltc_trades" }
15 | ])
16 |
--------------------------------------------------------------------------------
/presentation.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cbyn/bitpredict/8ea47d23d604c11bedb2be5b63d710460fe06d9d/presentation.pdf
--------------------------------------------------------------------------------