├── run.sh
├── run_bot.py
├── LICENSE.txt
├── setup.sh
├── parameters.py
├── README.md
└── helper_monkey.py
/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Run crypto bot script
4 |
5 |
6 | clear
7 |
8 | echo "======================================================="
9 | echo "============= Running Crypto Trading Bot =============="
10 | echo "======================================================="
11 |
12 | source crypto/bin/activate
13 |
14 | python3 run_bot.py
15 |
16 | exit 0
17 |
--------------------------------------------------------------------------------
/run_bot.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Sat Jan 9 09:35:49 2021
4 |
5 | @author: metalcorebear
6 | """
7 |
8 | import parameters
9 | import helper_monkey as mojo
10 |
11 | if __name__ == '__main__':
12 | mojo.main(parameters.API_KEY, pair=parameters.pair, granularity=parameters.granularity, duration=parameters.duration, cash_buffer=parameters.cash_buffer, reframe_threshold=parameters.reframe_threshold, continuous=parameters.continuous, chandelier=parameters.chandelier)
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Mark M. Bailey
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Install script for crypto trading bot libraries
4 | # Created on Sat Jan 9 09:56:48 2021
5 |
6 |
7 | clear
8 |
9 | echo "======================================================="
10 | echo "========= Instantiating Virtual Environment ==========="
11 | echo "======================================================="
12 |
13 | read -r -p "This script will make changes to your system which may break some applications and may require you to reimage your SD card. Are you sure that you wish to continue? [y/N] " confirm
14 |
15 | if ! [[ $confirm =~ ^([yY][eE][sS]|[yY])$ ]]
16 | then
17 | exit 1
18 | fi
19 |
20 | clear
21 | echo "Installing dos2Unix"
22 |
23 | sudo apt-get install dos2unix
24 |
25 | echo ""
26 | echo "Converting files to Unix"
27 |
28 | dos2unix *.py
29 | dos2unix *.md
30 | dos2unix *.txt
31 |
32 | echo ""
33 |
34 | echo "Building virtual environment and installing Python libraries"
35 |
36 | sudo apt install python3-pip
37 |
38 | pip3 install virtualenv
39 | virtualenv crypto
40 | source crypto/bin/activate
41 |
42 | sudo apt-get install libatlas-base-dev
43 |
44 | pip3 install cbpro==1.1.4
45 | pip3 install numpy==1.16.5
46 | pip3 install pandas==0.25.1
47 |
48 | echo "All relevant Python libraries installed"
49 |
50 | chmod +x run_bot.py
51 |
52 | echo "Script is now executable"
53 | echo "First update parameters.py file"
54 | echo "Then execute ./run_bot.py"
55 | echo "You are a great American!!"
56 |
57 | exit 0
58 |
--------------------------------------------------------------------------------
/parameters.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Fri Jan 8 11:17:23 2021
4 |
5 | @author: metalcorebear
6 | """
7 |
8 | """
9 | Contains all parameters for crypto trading bot.
10 |
11 | """
12 |
13 | """
14 | API Key parameters for Coinbase Pro.
15 | *** Ensure that all API permissions are enabled.***
16 |
17 | # key = Public Key (str)
18 | # secret = Secret Key (str)
19 | # passphrase = Passphrase (str)
20 |
21 | """
22 | API_KEY = {'key':'XXXXXXX', 'secret':'XXXXXXX', 'passphrase':'XXXXXXX'}
23 |
24 |
25 | """
26 | Other bot parameters.
27 |
28 | # API_KEY = parameters.API_KEY (dict)
29 | # pair = trading pair id (str)
30 | # granularity = interval time for each iteration in seconds. Default value is 15*60 (15 minutes). Note that this has not been tested for other durations, and shorter intervals may trigger API rate limitaitons and will reduce the overall timeframe of historic OHLC data used in strategy backtesting (due to the 300 value limit of the Coinbase Pro historic data API call). (int)
31 | # duration = time to run script in seconds (int)
32 | # cash_buffer = fraction of total cash to keep in account at any given time (float)
33 | # reframe_threshold = frequency of strategy reoptimization in hours (float)
34 | # continuous - set to True if script should run indefinitely (bool)
35 | # chandelier = use/don't use chandelier method for eATR calculation (bool)
36 |
37 | """
38 | pair = 'BTC-USD'
39 | granularity = 15*60
40 | duration = 2*60*60
41 | cash_buffer = 0.1
42 | reframe_threshold = 48.0
43 | continuous = False
44 | chandelier = False
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pi Trader
2 |
3 | (C) 2021 Mark M. Bailey, PhD
4 |
5 | ## About
6 |
7 | Pi Trader is a cryptocurrency trading bot for Raspberry Pi. This script instantiates a Raspberry Pi trading bot that interfaces with the Coinbase Pro API. First, it optimizes a trading strategy by backtesting against historic data from the Coinbase exchange. The strategy is based on buy/risk parameters defined as multiples of the Exponential Average True Range (eATR). OHLC data are refreshed each iteration, and a buy/sell signal is calculated and executed if appropriate. For buy signals, the maximum possible number of coins are purchased (with a user-specified fiat buffer preserved). For sell signals, all coins are exchanged for fiat. Strategy is re-optimized at user-defined intervals.
8 |
9 | This is what I would consider to be a "dumb" trading strategy based solely on price deviations from an indicator. I am curious about exploring machine learning solutions for trading strategy optimization. Future iterations of this project may include that as an option.
10 |
11 | Note that this is an experimental bot, and like all trading strategies, I can not guarantee that it will be profitable if implemented.
12 |
13 | If anyone is interested in making this project better, I'd be happy to collaborate.
14 |
15 | ## References
16 | * Carr, Michael. "Measure Volatility With Average True Range," *Investopedia,* Nov 2019, Link: https://www.investopedia.com/articles/trading/08/average-true-range.asp#:~:text=The%20average%20true%20range%20%28ATR%29%20is%20an%20exponential,signals%2C%20while%20shorter%20timeframes%20will%20increase%20trading%20activity.
17 | * Hall, Mary. "Enter Profitable Territory With Average True Range," *Investopedia,*" Sep 2020, Link: https://www.investopedia.com/articles/trading/08/atr.asp.
18 |
19 | ## Updates
20 | * 2021-01-09: Initial commit.
21 |
22 | ## Requirements
23 | * Raspberry Pi running Debian.
24 | * Configured WiFi adaptor or ethernet connection.
25 | * Active Coinbase Pro account API with all access permissions enabled.
26 | * Recommend installing on a fresh, fully-updated image of Debian.
27 |
28 | ## Installation
29 | * In terminal, navigate to folder and run `git clone https://github.com/metalcorebear/Pi-Trader.git`.
30 | * Navigate to folder and execute `sudo ./setup.sh` to set up the virtual environment.
31 | * Edit "parameters.py" file, as appropriate, with Coinbase API keys and other parameters (see below).
32 |
33 | ## Parameters
34 | API Key parameters for Coinbase Pro.
35 |
36 | * key = Public Key (str)
37 | * secret = Secret Key (str)
38 | * passphrase = Passphrase (str)
39 |
40 | Other bot parameters:
41 |
42 | * API_KEY = parameters.API_KEY (dict)
43 | * pair = trading pair id (str)
44 | * granularity = interval time for each iteration in seconds (int)
45 | * duration = time to run script in seconds (int)
46 | * cash_buffer = fraction of total cash to keep in account at any given time (float)
47 | * reframe_threshold = frequency of strategy reoptimization in hours (float)
48 | * continuous - set to True if script should run indefinitely (bool)
49 | * chandelier = use/don't use chandelier method for eATR calculation (bool)
50 |
51 | ## Execution
52 | * To execute, simply run `./run.sh`
53 |
--------------------------------------------------------------------------------
/helper_monkey.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Fri Jan 8 11:25:49 2021
4 |
5 | @author: metalcorebear
6 | """
7 |
8 | import cbpro
9 | import numpy as np
10 | import pandas as pd
11 | from datetime import datetime
12 | from datetime import timedelta
13 | import time
14 |
15 | # data acquistion functions
16 |
17 | def get_product_data(pair):
18 | output = {}
19 | public_client = cbpro.PublicClient()
20 | products = public_client.get_products()
21 | for item in products:
22 | if item['id'] == pair:
23 | output.update(item)
24 | return output
25 |
26 | def get_historic_data(pair='BTC-USD', granularity=900, **options):
27 | public_client = cbpro.PublicClient()
28 | history = public_client.get_product_historic_rates(pair, granularity=granularity)
29 | history_array = np.array(history)
30 | history_pd = pd.DataFrame(history_array, columns=['time', 'low', 'high', 'open', 'close', 'volume'])
31 | return history_pd, history_array
32 |
33 | def get_latest(pair='BTC-USD', granularity=900):
34 | public_client = cbpro.PublicClient()
35 | start = datetime.now()
36 | end = start + timedelta(minutes=int(granularity/60))
37 | history = public_client.get_product_historic_rates(pair, granularity=granularity, start=str(start.isoformat()), end=str(end.isoformat()))
38 | #history_array = np.array(history)
39 | return history
40 |
41 | def new_history(history_array, history):
42 | history_array_list = history_array.tolist()
43 | history.extend(history_array_list)
44 | history_array = np.array(history)
45 | #history = np.array(history, dtype=history_array.dtype)
46 | #history_array = np.concatenate((history, history_array), axis=0)
47 | history_array = history_array[:-1,:]
48 | return history_array
49 |
50 |
51 | # Trading functions
52 |
53 | def make_trade(pair, amount, trade_type, key, secret, passphrase):
54 | auth_client = cbpro.AuthenticatedClient(key, secret, passphrase)
55 | if trade_type == 'buy':
56 | response = auth_client.buy(order_type='market', product_id=pair, funds=amount)
57 | if trade_type == 'sell':
58 | response = auth_client.sell(order_type='market', product_id=pair, size=amount)
59 | return response
60 |
61 | def check_order_status(response, key, secret, passphrase):
62 | auth_client = cbpro.AuthenticatedClient(key, secret, passphrase)
63 | order_id = response['id']
64 | output = True
65 | if order_id is not None:
66 | check = auth_client.get_order(order_id)
67 | output = check['settled']
68 | return output
69 |
70 | def get_currency_balance(currency, key, secret, passphrase):
71 | auth_client = cbpro.AuthenticatedClient(key, secret, passphrase)
72 | response = auth_client.get_accounts()
73 | for item in response:
74 | if item['currency'] == currency:
75 | output = float(item['available'])
76 | return output
77 |
78 | # strategy functions
79 |
80 | def reframe_data(data):
81 | # history = [time, low, high, open, close, volume]
82 | data.rename(columns={'low':'low', 'high':'high', 'open':'Open', 'close':'Adj Close'}, inplace=True)
83 | data = data[['Open', 'high', 'low', 'Adj Close']]
84 | data = data.iloc[::-1]
85 | data = data.values
86 | data = pd.DataFrame(data, columns=['Open', 'high', 'low', 'Adj Close'])
87 | return data
88 |
89 | def eATR(data, lookback=10):
90 | m = data.values
91 | z = np.zeros((m.shape[0], 2))
92 | m = np.concatenate((m, z), axis=1)
93 | columns = ['Open', 'high', 'low', 'Adj Close', 'ATR', 'eATR']
94 | # calculate ATR values
95 | for i in range(1, len(m)):
96 | atr = [m[i,1] - m[i,2], abs(m[i,1] - m[i-1,3]), abs(m[i-1,3] - m[i,2])]
97 | m[i,4] = max(atr)
98 | # calcualate exponential moving average
99 | alpha = 2.0/float(lookback+1.0)
100 | sma = sum(m[:lookback,4]) / float(lookback)
101 | m[lookback,5] = sma
102 | for i in range(1,len(m)-lookback):
103 | m[i+lookback,5] = m[i+lookback,4]*alpha + m[i-1+lookback,5]*(1.0-alpha)
104 | out = pd.DataFrame(m, columns=columns, index=data.index)
105 | return out
106 |
107 | def strategize(data, strategy={'buy':1.0, 'risk':1.0}, chandelier=False):
108 | m = data.values
109 | z = np.zeros((m.shape[0], 2))
110 | m = np.concatenate((m, z), axis=1)
111 | columns = ['Open', 'high', 'low', 'Adj Close', 'ATR', 'eATR', 'buy_point', 'sell_point']
112 | for i in range(1, len(m)):
113 | if (m[i,3] > (m[i-1,3] + strategy['buy']*m[i-1,5])) and (m[i-1,5]>0):
114 | m[i,6] = 1
115 | if chandelier:
116 | if (m[i,3] < (m[i-1,1] - strategy['risk']*m[i-1,5])) and (m[i-1,5]>0):
117 | m[i,7] = 1
118 | else:
119 | if (m[i,3] < (m[i-1,3] - strategy['risk']*m[i-1,5])) and (m[i-1,5]>0):
120 | m[i,7] = 1
121 | out = pd.DataFrame(m, columns=columns, index=data.index)
122 | return out
123 |
124 | def evaluate(data, risk_factor=1.0, chandelier=False):
125 | m = data.values
126 | profits = []
127 | risk_rewards = []
128 | winning = 0
129 | all_trades = 0
130 |
131 | j = 0
132 | j_start = 1
133 | first_buy = []
134 |
135 | for i in range(1,len(m)-1):
136 |
137 | if m[i,6] == 1:
138 | buy = m[i,3]
139 | if len(first_buy) == 0:
140 | first_buy.append(buy)
141 | if j_start < i:
142 | j_start = i+1
143 | else:
144 | j_start = j
145 | for j in range(j_start,len(m)):
146 | if m[j,7] == 1:
147 | sell = m[j,3]
148 | all_trades += 1
149 | profit = sell-buy
150 | profits.append(profit)
151 | if chandelier:
152 | stop = m[i-1,1] - risk_factor*m[i,5]
153 | else:
154 | stop = m[i-1,3] - risk_factor*m[i,5]
155 | risk_reward = profit / (buy - stop)
156 | risk_rewards.append(risk_reward)
157 | if profit > 0:
158 | winning += 1
159 | break
160 |
161 | if len(profits) != 0:
162 | expected = np.average(np.array(profits))
163 | total_profit = sum(profits)
164 | ROI = round(total_profit / first_buy[0],2)
165 | else:
166 | expected = 0.0
167 | total_profit = 0.0
168 | ROI = 0.0
169 | total_profit = sum(profits)
170 | profits = np.array(profits)
171 | equity_curve = profits.cumsum()
172 | if all_trades != 0:
173 | hit_ratio = round(float(winning) / float(all_trades), 2)
174 | else:
175 | hit_ratio = 0.0
176 | gross_profits = [k for k in profits if k > 0]
177 | gross_losses = [abs(k) for k in profits if k < 0]
178 | if sum(gross_losses) != 0.0:
179 | profit_factor = round(sum(gross_profits) / sum(gross_losses), 2)
180 | else:
181 | profit_factor = np.nan
182 | if len(risk_rewards) != 0:
183 | risk_reward = np.average(np.array(risk_rewards))
184 | else:
185 | risk_reward = 0.0
186 | output = {'hit_ratio':round(hit_ratio,2), 'total_trades':all_trades, 'expected':round(expected,2), 'total_profit':round(total_profit,2), 'ROI':ROI, 'profit_factor':round(profit_factor,2), 'risk_ratio':round(risk_reward,2), 'equity_curve':equity_curve}
187 | return output
188 |
189 | def simulate_strategies(data, buy_range = (1.0, 4.0, 0.25), risk_range=(1.0, 4.0, 0.25), chandelier=False):
190 | buy_i = (buy_range[1] - buy_range[0])/buy_range[2]
191 | buy_test = [buy_range[0] + float(i)*buy_range[2] for i in range(int(buy_i)+1)]
192 | risk_i = (risk_range[1] - risk_range[0])/risk_range[2]
193 | risk_test = [risk_range[0] + float(i)*risk_range[2] for i in range(int(risk_i)+1)]
194 | strategies = []
195 |
196 | for i in buy_test:
197 | for j in risk_test:
198 | s = {'buy':i, 'risk':j}
199 | strategies.append(s)
200 |
201 | for i in range(len(strategies)):
202 | strategy = strategies[i]
203 | eatr = eATR(data)
204 | strat = strategize(eatr, strategy=strategy, chandelier=chandelier)
205 | sim = evaluate(strat, risk_factor=strategy['risk'], chandelier=chandelier)
206 | strategy.update(sim)
207 | return strategies
208 |
209 | def find_optimal_strategy(strategies):
210 | previous_number_to_beat = 0.0
211 | best_strategy = {'buy':1.0,'risk':1.0}
212 | for strategy in strategies:
213 | profit = strategy['total_profit']
214 | if profit > previous_number_to_beat:
215 | best_strategy = strategy
216 | previous_number_to_beat = profit
217 | return best_strategy
218 |
219 | def optimize_strategy(data, buy_range = (1.0, 4.0, 0.25), risk_range=(1.0, 4.0, 0.25), chandelier=False):
220 | strategies = simulate_strategies(data, buy_range = (1.0, 4.0, 0.25), risk_range=(1.0, 4.0, 0.25), chandelier=chandelier)
221 | best_strategy = find_optimal_strategy(strategies)
222 | return best_strategy
223 |
224 | def iterate_signal(history_array, strategy, pair='BTC-USD', granularity=900, chandelier=False):
225 | history = get_latest(pair=pair, granularity=granularity)
226 | history_array = new_history(history_array, history)
227 | history_pd = pd.DataFrame(history_array, columns=['time', 'low', 'high', 'open', 'close', 'volume'])
228 | history_pd = reframe_data(history_pd)
229 | history_pd = eATR(history_pd, lookback=10)
230 | history_pd = strategize(history_pd, strategy=strategy, chandelier=chandelier)
231 | end_point = len(history_pd) - 1
232 | buy_point, sell_point = history_pd['buy_point'][end_point], history_pd['sell_point'][end_point]
233 | if buy_point == 1:
234 | buy_point = True
235 | else:
236 | buy_point = False
237 | if sell_point == 1:
238 | sell_point = True
239 | else:
240 | sell_point = False
241 | return history_array, buy_point, sell_point
242 |
243 |
244 | # MAIN FUNCTION
245 |
246 | def main(API_KEY, pair='BTC-USD', granularity=900, duration=7*24*60*60, cash_buffer=0.1, reframe_threshold=48.0, continuous=False, chandelier=False):
247 |
248 | key, secret, passphrase = API_KEY['key'], API_KEY['secret'], API_KEY['passphrase']
249 | print('Initializing optimal trading strategy...')
250 |
251 | # Get initial optimal strategy
252 | history_pd, history_array = get_historic_data(pair=pair, granularity=granularity)
253 | reframed = reframe_data(history_pd)
254 | best_strategy = optimize_strategy(reframed, buy_range = (1.0, 4.0, 0.25), risk_range=(1.0, 4.0, 0.25), chandelier=False)
255 |
256 | # Initializing timestamp
257 |
258 | running = True
259 | total_cycles = int(duration/granularity)
260 | cycle = 0
261 |
262 | total_hours = duration/3600
263 |
264 | t = 0
265 | hour = 1.0
266 |
267 | response = {'id':None}
268 |
269 | while running:
270 |
271 | # Timecheck and reoptimization check
272 | cycle += 1
273 | if cycle >= total_cycles:
274 | if not continuous:
275 | print('{}: Duration exceeded.'.format(str(round(t))))
276 | t += 1
277 | running = False
278 | else:
279 | running = True
280 | elapsed_hours = (cycle/total_cycles)*total_hours
281 | if (elapsed_hours - hour) >= reframe_threshold:
282 | print('{}: Reoptimizing trading strategy...'.format(str(t)))
283 | t += 1
284 | hour = hour + 1.0
285 | history_pd, history_array = get_historic_data(pair=pair, granularity=granularity)
286 | reframed = reframe_data(history_pd)
287 | best_strategy = optimize_strategy(reframed, buy_range = (1.0, 4.0, 0.25), risk_range=(1.0, 4.0, 0.25), chandelier=False)
288 |
289 | # Check crypto status
290 | output = get_product_data(pair)
291 | iteration = 0
292 | while output['status'] != 'online':
293 | print('{}: Crypto status error. Waiting...'.format(str(round(t))))
294 | t += 1
295 | print('Waiting for {} seconds...'.format(str(granularity)))
296 | time.sleep(granularity)
297 | cycle += 1
298 | history_array, buy_point, sell_point = iterate_signal(history_array, best_strategy, pair=pair, granularity=granularity, chandelier=chandelier)
299 | output = get_product_data(pair)
300 | iteration += 1
301 | if iteration == 10:
302 | print('{}: Crypto availability timeout...try again later.'.format(str(round(t))))
303 | t += 1
304 | running = False
305 | continue
306 |
307 | # Iterate price array
308 | history_array, buy_point, sell_point = iterate_signal(history_array, best_strategy, pair=pair, granularity=granularity, chandelier=chandelier)
309 |
310 | if sell_point:
311 | balance = get_currency_balance('BTC', key, secret, passphrase)
312 | if balance > float(output['base_min_size']):
313 | response = make_trade(pair, balance, 'sell', key, secret, passphrase)
314 | print('{}: Sold {} crypto.'.format(str(t), str(balance)))
315 | t += 1
316 | else:
317 | print('{}: No crypto to sell.'.format(str(t)))
318 | t += 1
319 |
320 | elif buy_point:
321 | balance = get_currency_balance('USD', key, secret, passphrase)
322 | if balance > 6.0: # Since the minimum buy amount is subject to change in the future this is a hotfix, and could be better solved by doing base_min_size * current_BTC_price
323 | tender = round((1.0-cash_buffer)*balance,2)
324 | response = make_trade(pair, tender, 'buy', key, secret, passphrase)
325 | print('{}: Purchased BTC for ${}.'.format(str(t), str(round(tender,2))))
326 | t += 1
327 | else:
328 | print('{}: Not enough fiat to buy.'.format(str(t)))
329 | t += 1
330 |
331 | else:
332 | print('{}: No transaction point this iteration.'.format(str(t)))
333 | t += 1
334 |
335 | print('Waiting for {} seconds...'.format(str(granularity)))
336 | time.sleep(granularity)
337 | cycle += 1
338 | history_array, buy_point, sell_point = iterate_signal(history_array, best_strategy, pair=pair, granularity=granularity, chandelier=chandelier)
339 |
340 | # Check transaction status
341 | cleared = check_order_status(response, key, secret, passphrase)
342 | while cleared == False:
343 | print('{}: Still waiting for transaction to settle...'.format(str(t)))
344 | t += 1
345 | print('Waiting for {} seconds...'.format(str(granularity)))
346 | time.sleep(granularity)
347 | cycle += 1
348 | history_array, buy_point, sell_point = iterate_signal(history_array, best_strategy, pair=pair, granularity=granularity, chandelier=chandelier)
349 | cleared = check_order_status(response, key, secret, passphrase)
350 | continue
351 |
352 | running = True
353 | continue
354 |
355 | print('{}: Session terminated.'.format(str(t)))
356 |
--------------------------------------------------------------------------------