├── .gitignore
├── analysis
├── __init__.py
└── chart.py
├── signals.db
├── screenshots
├── settings.jpg
├── positions.png
├── mainscreen_top.jpg
└── mainscreen_bottom.jpg
├── templates
├── home.html
├── signals.html
├── settings.html
├── wavetrade.html
├── index.html
└── quicktrade.html
├── requirements.txt
├── static
├── package.json
├── src
│ └── tailwind_src.css
├── js
│ ├── waves.js
│ └── index.js
└── package-lock.json
├── main.py
├── thread_manager.py
├── README.md
├── heiken_ashi.py
├── utils.py
├── signal_data.py
├── app.py
├── signals_loop.py
├── charts.py
├── algo_trader.py
├── async_signals.py
├── signals.py
├── coin_data.py
├── trader.py
└── async_algo_trader.py
/.gitignore:
--------------------------------------------------------------------------------
1 | /venv/
2 |
--------------------------------------------------------------------------------
/analysis/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/signals.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihilok/perpSniper/HEAD/signals.db
--------------------------------------------------------------------------------
/screenshots/settings.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihilok/perpSniper/HEAD/screenshots/settings.jpg
--------------------------------------------------------------------------------
/screenshots/positions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihilok/perpSniper/HEAD/screenshots/positions.png
--------------------------------------------------------------------------------
/screenshots/mainscreen_top.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihilok/perpSniper/HEAD/screenshots/mainscreen_top.jpg
--------------------------------------------------------------------------------
/screenshots/mainscreen_bottom.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihilok/perpSniper/HEAD/screenshots/mainscreen_bottom.jpg
--------------------------------------------------------------------------------
/templates/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | ADD QUICKTRADE PAIR
167 |
168 |
169 |
170 |
171 |
172 |
185 |
186 |
187 |
188 |
190 | Settings
191 |
192 |
193 |
194 |
195 |
BTCUSDT
196 |
1h
197 |
198 |
199 |
203 |
204 |
236 |
237 |
--------------------------------------------------------------------------------
/signals_loop.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import io
3 | import os
4 | import sqlite3
5 | import time
6 | import logging
7 |
8 | from collections import Counter
9 |
10 | from datetime import datetime, timedelta
11 |
12 | from apscheduler.schedulers.background import BackgroundScheduler
13 | from binance.exceptions import BinanceAPIException
14 |
15 | from signals import Signals
16 | from coin_data import CoinData
17 |
18 |
19 | scheduler = BackgroundScheduler()
20 |
21 | file_path = os.path.abspath(os.path.dirname(__file__))
22 | os.chdir(file_path)
23 |
24 | # flush log file
25 | try:
26 | with open('log.log', 'r') as f:
27 | lines = f.readlines()
28 | if len(lines) >= 88:
29 | with open('log.log', 'w') as f:
30 | f.writelines(lines[-88:])
31 | except FileNotFoundError:
32 | pass
33 |
34 | logger = logging.getLogger(__name__)
35 | logging.basicConfig(level=logging.DEBUG, filename='log.log')
36 |
37 |
38 | class MainLoop:
39 |
40 | def __init__(self, coin_data):
41 |
42 | """Check signals for all coins passed from coin_data. Also includes tear down method and background tasks."""
43 |
44 | os.system('cls' if os.name == 'nt' else 'clear')
45 | print('15m, 1h & 4h signals update every minute to offer an early warning of events occuring on current ('
46 | 'unclosed) candle\'s close. **Expect false positives**\nIf signals are still true they '
47 | 'are only repeated after 30 minutes\nHint: Look for volume signals to confirm other signals')
48 | self.data_lock = True
49 | self.data = coin_data
50 | self.coins = self.data.symbols
51 | try:
52 | self.conn = self.open_database()
53 | self.flush_db()
54 | self.data_lock = False
55 | except Exception as e:
56 | raise e
57 |
58 | def open_database(self):
59 | path = os.path.abspath(os.path.dirname(__file__))
60 | file = os.path.join(path, 'signals.db')
61 | self.conn = sqlite3.connect(file)
62 | return self.conn
63 |
64 | def close_database(self):
65 | if self.conn:
66 | self.conn.close()
67 |
68 | def get_qs(self):
69 | self.open_database()
70 | c = self.conn.cursor()
71 | qs = [a for a in c.execute('SELECT * FROM signals')]
72 | c.close()
73 | self.close_database()
74 | return qs
75 |
76 | def flush_db(self):
77 | alert_times = []
78 | qs = self.get_qs()
79 | for alert in qs:
80 | if self.check_time(alert):
81 | alert_times.append(f'{alert[0]}',)
82 | at = tuple(alert_times)
83 | self.delete_multiple_records(at)
84 |
85 | @staticmethod
86 | def check_time(alert, minutes=30):
87 | full_time = datetime.strptime(datetime.now().strftime('%Y-%m-%d ') + alert[0],
88 | '%Y-%m-%d %H:%M:%S')
89 | if full_time < datetime.now() - timedelta(minutes=minutes) or full_time > datetime.now():
90 | return True
91 |
92 | def delete_multiple_records(self, idList):
93 | try:
94 | self.open_database()
95 | c = self.conn.cursor()
96 | idList = [(a, ) for a in idList]
97 | sqlite_update_query = f"""DELETE from signals where time = ?"""
98 | c.executemany(sqlite_update_query, idList)
99 | self.conn.commit()
100 | c.close()
101 |
102 | except sqlite3.Error as error:
103 | print("Failed to delete multiple records from sqlite table", error)
104 | finally:
105 | self.close_database()
106 |
107 | def get_popular_coins(self):
108 | self.data = CoinData()
109 | self.coins = self.data.symbols
110 | print(f'''Current Symbols (highest volume):
111 | {', '.join(self.coins)}''')
112 | return self.coins
113 |
114 | def register_alert(self, alert, coin, tf):
115 | try:
116 | self.open_database()
117 | c = self.conn.cursor()
118 | c.execute(f'''INSERT INTO signals VALUES
119 | ("{alert[0]}", "{coin}", "({tf}) {alert[1]}{' ' + alert[2] if len(alert) == 3 else ''}")''')
120 | self.conn.commit()
121 | c.close()
122 | except sqlite3.Error as error:
123 | print("Failed to register alert for " + coin, error)
124 | finally:
125 | self.close_database()
126 |
127 | def check_alert(self, alert, coin, tf):
128 | self.open_database()
129 | c = self.conn.cursor()
130 | qs = c.execute(f'''SELECT * FROM signals WHERE symbol="{coin}" AND alert="({tf}) {alert[1]}{' ' + alert[2] if len(alert) == 3 else ''}"''')
131 | qs = [a for a in qs]
132 | c.close()
133 | self.close_database()
134 | if len(qs):
135 | return True
136 | return False
137 |
138 | def check_hot_coins(self):
139 | self.open_database()
140 | c = self.conn.cursor()
141 | qs = c.execute(f'''SELECT * FROM signals''')
142 |
143 | symbols = []
144 | hot_coins = []
145 | for rec in qs:
146 | symbols.append(rec[1])
147 | C = Counter(symbols)
148 | for item in C.items():
149 | if item[1] >= 3:
150 | hot_coins.append(item)
151 | c.close()
152 | self.close_database()
153 | return list(sorted(hot_coins, key=lambda x: x[1], reverse=True))[:6]
154 |
155 | def mainloop(self):
156 | bad_coins = []
157 | for coin in self.coins:
158 | check_time = datetime.now()
159 | try:
160 | signals_15m = Signals(coin, tf='15m')
161 | self.check_signals_object(signals_15m, coin, check_time)
162 | except BinanceAPIException as e:
163 | print(f'Something went wrong with {coin}: {e}')
164 | continue
165 | except IndexError:
166 | bad_coins.append(coin)
167 | continue
168 | try:
169 | signals_1h = Signals(coin, tf='1h')
170 | self.check_signals_object(signals_1h, coin, check_time)
171 | except BinanceAPIException as e:
172 | print(f'Something went wrong with {coin}: {e}')
173 | continue
174 | except IndexError:
175 | pass
176 | try:
177 | signals_4h = Signals(coin)
178 | self.check_signals_object(signals_4h, coin, check_time)
179 | except BinanceAPIException as e:
180 | print(f'Something went wrong with {coin}: {e}')
181 | except IndexError:
182 | pass
183 | self.flush_db()
184 | for coin in bad_coins:
185 | self.coins.remove(coin)
186 |
187 | def check_signals_object(self, signals_obj, coin, check_time):
188 | for k, v in signals_obj.ema_signals_dict.items():
189 | if v is True:
190 | alert = (check_time.strftime("%H:%M:%S"), k, 'bullish')
191 | if not self.check_alert(alert, coin, signals_obj.tf):
192 | self.register_alert(alert, coin, signals_obj.tf)
193 | elif v is False:
194 | alert = (check_time.strftime("%H:%M:%S"), k, 'bearish')
195 | if not self.check_alert(alert, coin, signals_obj.tf):
196 | self.register_alert(alert, coin, signals_obj.tf)
197 | for k, v in signals_obj.rsi_div_dict.items():
198 | if v is True:
199 | alert = (check_time.strftime("%H:%M:%S"), k)
200 | if not self.check_alert(alert, coin, signals_obj.tf):
201 | self.register_alert(alert, coin, signals_obj.tf)
202 | for k, v in signals_obj.rsi_ob_os_dict.items():
203 | if v is True:
204 | alert = (check_time.strftime("%H:%M:%S"), k)
205 | if not self.check_alert(alert, coin, signals_obj.tf):
206 | self.register_alert(alert, coin, signals_obj.tf)
207 | for k, v in signals_obj.macd_dict.items():
208 | if v is not None:
209 | if v is True:
210 | v = 'up'
211 | else:
212 | v = 'down'
213 | alert = (check_time.strftime("%H:%M:%S"), ' '.join((k, v)))
214 | if not self.check_alert(alert, coin, signals_obj.tf):
215 | self.register_alert(alert, coin, signals_obj.tf)
216 | if signals_obj.vol_signal:
217 | alert = (check_time.strftime("%H:%M:%S"), 'Volume rising')
218 | if not self.check_alert(alert, coin, signals_obj.tf):
219 | self.register_alert(alert, coin, signals_obj.tf)
220 | if signals_obj.vol_candle:
221 | alert = (check_time.strftime("%H:%M:%S"), 'Current candle large volume')
222 | if not self.check_alert(alert, coin, signals_obj.tf):
223 | self.register_alert(alert, coin, signals_obj.tf)
224 |
225 | def start_jobs(self, jobs=None):
226 | """:param jobs: list of tuples (job, trigger, interval)"""
227 | scheduler.start()
228 | scheduler.add_job(self.mainloop, trigger="cron", minute='*/1')
229 | # scheduler.add_job(self.get_popular_coins, trigger="cron", hour='*/1')
230 | if jobs:
231 | for job in jobs:
232 | if job[1] == 'interval':
233 | scheduler.add_job(job[0], trigger=job[1], seconds=job[2])
234 | elif job[1] == 'cron':
235 | scheduler.add_job(job[0], trigger=job[1], second=job[2])
236 |
237 | def teardown(self):
238 | self.close_database()
239 | print('Signals loop teardown completed')
240 |
241 |
242 | # if __name__ == '__main__':
243 | # loop = MainLoop()
244 | # try:
245 | # loop.check_hot_coins()
246 | # loop.mainloop()
247 | # loop.start_jobs()
248 | # while True:
249 | # time.sleep(1)
250 | # finally:
251 | # loop.teardown()
252 |
--------------------------------------------------------------------------------
/charts.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import io
3 | import sys
4 |
5 | import matplotlib
6 |
7 | matplotlib.use('agg')
8 |
9 | import mplfinance as mpf
10 | import pandas as pd
11 | import pandas_ta as ta
12 |
13 | from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
14 |
15 | from matplotlib import pyplot as plt
16 |
17 | from coin_data import CoinData
18 |
19 | plt.style.use('dark_background')
20 | import numpy as np
21 | import numba as nb
22 |
23 | @nb.jit(fastmath=True, nopython=True)
24 | def calc_rsi( array, deltas, avg_gain, avg_loss, n ):
25 |
26 | # Use Wilder smoothing method
27 | up = lambda x: x if x > 0 else 0
28 | down = lambda x: -x if x < 0 else 0
29 | i = n+1
30 | for d in deltas[n+1:]:
31 | avg_gain = ((avg_gain * (n-1)) + up(d)) / n
32 | avg_loss = ((avg_loss * (n-1)) + down(d)) / n
33 | if avg_loss != 0:
34 | rs = avg_gain / avg_loss
35 | array[i] = 100 - (100 / (1 + rs))
36 | else:
37 | array[i] = 100
38 | i += 1
39 |
40 | return array
41 |
42 | def get_rsi( array, n = 14 ):
43 |
44 | deltas = np.append([0],np.diff(array))
45 |
46 | avg_gain = np.sum(deltas[1:n+1].clip(min=0)) / n
47 | avg_loss = -np.sum(deltas[1:n+1].clip(max=0)) / n
48 |
49 | array = np.empty(deltas.shape[0])
50 | array.fill(np.nan)
51 |
52 | array = calc_rsi( array, deltas, avg_gain, avg_loss, n )
53 | return array
54 |
55 | class Charts:
56 |
57 | def __init__(self, symbol='BTCUSDT', tf='1h', entry=None, direction=None):
58 | """When initialised, creates chart for given symbol and timeframe
59 | :param symbol: str uppercase eg. 'BTCUSDT'
60 | :param tf: str lowercase eg. '1m', '15m', '1h', '4h'
61 | :param entry: float if open positions
62 | :param direction: str 'LONG', 'SHORT' or None depending on position"""
63 | self.symbol = symbol.upper()
64 | self.tf = tf
65 | self.df = CoinData.get_dataframe(self.symbol, self.tf)
66 | self.df = self.df_ta()
67 | self.entry = entry
68 | self.direction = True if direction == 'LONG' else False
69 | self.tp = 0.03
70 | self.sl = 0.01
71 |
72 | def df_ta(self) -> pd.DataFrame:
73 | df = self.df
74 | # df['rsi'] = ta.rsi(df.close, 14)
75 | df['rsi'] = get_rsi(df.close, 14)
76 | df = pd.concat((df, ta.macd(df.close, 12, 26, 9)), axis=1)
77 | df['ema_20'], df['ema_50'] = ta.ema(df.close, 20), ta.ema(df.close, 50)
78 | if len(df) >= 288:
79 | df['ema_200'] = ta.ema(df.close, 200)
80 | else:
81 | df['ema_200'] = ta.ema(df.close, len(df.close) - 3)
82 | df = df.tail(88)
83 | return df
84 |
85 | @staticmethod
86 | def get_rsi_timeseries(prices, n=14):
87 | # RSI = 100 - (100 / (1 + RS))
88 | # where RS = (Wilder-smoothed n-period average of gains / Wilder-smoothed n-period average of -losses)
89 | # Note that losses above should be positive values
90 | # Wilder-smoothing = ((previous smoothed avg * (n-1)) + current value to average) / n
91 | # For the very first "previous smoothed avg" (aka the seed value), we start with a straight average.
92 | # Therefore, our first RSI value will be for the n+2nd period:
93 | # 0: first delta is nan
94 | # 1:
95 | # ...
96 | # n: lookback period for first Wilder smoothing seed value
97 | # n+1: first RSI
98 |
99 | # First, calculate the gain or loss from one price to the next. The first value is nan so replace with 0.
100 | deltas = (prices - prices.shift(1)).fillna(0)
101 |
102 | # Calculate the straight average seed values.
103 | # The first delta is always zero, so we will use a slice of the first n deltas starting at 1,
104 | # and filter only deltas > 0 to get gains and deltas < 0 to get losses
105 | avg_of_gains = deltas[1:n + 1][deltas > 0].sum() / n
106 | avg_of_losses = -deltas[1:n + 1][deltas < 0].sum() / n
107 |
108 | # Set up pd.Series container for RSI values
109 | rsi_series = pd.Series(0.0, deltas.index)
110 |
111 | # Now calculate RSI using the Wilder smoothing method, starting with n+1 delta.
112 | up = lambda x: x if x > 0 else 0
113 | down = lambda x: -x if x < 0 else 0
114 | i = n + 1
115 | for d in deltas[n + 1:]:
116 | avg_of_gains = ((avg_of_gains * (n - 1)) + up(d)) / n
117 | avg_of_losses = ((avg_of_losses * (n - 1)) + down(d)) / n
118 | if avg_of_losses != 0:
119 | rs = avg_of_gains / avg_of_losses
120 | rsi_series[i] = 100 - (100 / (1 + rs))
121 | else:
122 | rsi_series[i] = 100
123 | i += 1
124 |
125 | return rsi_series
126 |
127 |
128 | def main_chart(self):
129 | fig, axes = plt.subplots(nrows=3, ncols=1, gridspec_kw={'height_ratios': [3, 1, 1]})
130 | fig.suptitle(f"{self.symbol} {self.tf}", fontsize=16)
131 | ax_r = axes[0].twinx()
132 | mc = mpf.make_marketcolors(up='#00e600', down='#ff0066',
133 | edge={'up': '#00e600', 'down': '#ff0066'},
134 | wick={'up': '#00e600', 'down': '#ff0066'},
135 | volume={'up': '#808080', 'down': '#4d4d4d'},
136 | ohlc='black')
137 | s = mpf.make_mpf_style(marketcolors=mc)
138 | ax_r.set_alpha(0.01)
139 | axes[0].set_zorder(2)
140 | for ax in axes:
141 | ax.set_facecolor((0, 0, 0, 0))
142 | ax_r.set_zorder(1)
143 |
144 | axes[1].set_ylabel('RSI')
145 | axes[1].margins(x=0, y=0.1)
146 | axes[0].margins(x=0, y=0.05)
147 | axes[2].set_ylabel('MACD')
148 | ax_r.set_ylabel('')
149 | ax_r.yaxis.set_visible(False)
150 | axes[2].margins(0, 0.05)
151 | axes[0].xaxis.set_visible(False)
152 | axes[1].xaxis.set_visible(False)
153 |
154 | axes[0].yaxis.tick_left()
155 | axes[0].yaxis.set_label_position('right')
156 | axes[1].yaxis.set_label_position('right')
157 | axes[2].yaxis.set_label_position('right')
158 | plt.tight_layout()
159 | fig.autofmt_xdate()
160 | self.df.volume = self.df.volume.div(2)
161 | addplot_200 = mpf.make_addplot(self.df['ema_200'], type='line', ax=axes[0], width=1, color='#ff0066')
162 | addplot_50 = mpf.make_addplot(self.df['ema_50'], type='line', ax=axes[0], width=1, color='#00e600')
163 | mpf.plot(self.df, ax=axes[0], type="candle", style=s, volume=ax_r, ylabel='', addplot=[addplot_200, addplot_50])
164 | max_vol = max({y for index, y in self.df.volume.items()})
165 | ax_r.axis(ymin=0, ymax=max_vol * 3)
166 | self.df['rsi'].plot(ax=axes[1], legend=False, use_index=True, sharex=axes[0], color='#00e600')
167 | self.df['MACD_12_26_9'].plot(ax=axes[2], legend=False, use_index=True, sharex=axes[0], color='#00e600')
168 | self.df['MACDs_12_26_9'].plot(ax=axes[2], legend=False, use_index=True, sharex=axes[0], color='#ff0066')
169 | axes[2].axhline(0, color='gray', ls='--', linewidth=1)
170 | axes[1].axhline(70, color='gray', ls='--', linewidth=1)
171 | axes[1].axhline(30, color='gray', ls='--', linewidth=1)
172 | if self.entry:
173 | tp = self.entry + self.entry * self.tp if self.direction else self.entry - self.entry * self.tp
174 | sl = self.entry - self.entry * self.sl if self.direction else self.entry + self.entry * self.sl
175 | tp_color = 'red' if self.direction else 'green'
176 | sl_color = 'red' if not self.direction else 'green'
177 | axes[0].axhline(self.entry, color='yellow', ls="--", linewidth=.5)
178 | axes[0].axhline(tp, color=tp_color, ls="--", linewidth=.5)
179 | axes[0].axhline(sl, color=sl_color, ls="--", linewidth=.5)
180 | axes[2].set_xlabel('')
181 | img = io.BytesIO()
182 | FigureCanvas(fig).print_png(img)
183 | plot_url = base64.b64encode(img.getvalue()).decode()
184 | fig.savefig('plot.png', format='png')
185 | plt.close(fig)
186 | return plot_url
187 |
188 | # def plot_rsi_div(self):
189 | # rsi_array = np.array(self.df['rsi'].tail(20).array)
190 | # close_array = np.array(self.df['close'].tail(20).array)
191 | # rsi_peaks, _ = scipy.signal.find_peaks(rsi_array)
192 | # rsi_troughs, _ = scipy.signal.find_peaks(-rsi_array)
193 | # fig, (ax1, ax2) = plt.subplots(2, sharex=True)
194 | # fig.suptitle(f'{self.symbol} RSI Divergence {self.tf}')
195 | # ax1.set_ylabel('Close')
196 | # ax2.set_ylabel('RSI')
197 | # ax2.axhline(70, color='gray', ls='--')
198 | # ax2.axhline(30, color='gray', ls='--')
199 | # ax1.xaxis.set_visible(False)
200 | # ax2.xaxis.set_visible(False)
201 | # ax1.plot(close_array)
202 | # ax2.plot(rsi_array, color='green')
203 | # ax1.plot(rsi_peaks, close_array[rsi_peaks], '.', color="#ff0066")
204 | # ax2.plot(rsi_peaks, rsi_array[rsi_peaks], '.', color="#ff0066")
205 | # ax1.plot(rsi_troughs, close_array[rsi_troughs], '.', color="#00e600")
206 | # ax2.plot(rsi_troughs, rsi_array[rsi_troughs], '.', color="#00e600")
207 | # _, new_close_array, new_rsi_array, indices = self.rsi_divergence()
208 | # if len(close_array) != len(new_close_array):
209 | # ax1.plot(indices, new_close_array, color="#ff0066")
210 | # ax2.plot(indices, new_rsi_array, color="#ff0066")
211 | # img = io.BytesIO()
212 | # fig.savefig(img, format='png')
213 | # img.seek(0)
214 | # plot_url = base64.b64encode(img.getvalue()).decode()
215 | # plt.close()
216 | # return plot_url
217 |
218 | def plot_charts(self):
219 | self.main_chart()
220 | # self.plot_rsi_div()
221 |
222 |
223 | if __name__ == '__main__':
224 | c = Charts('SNXUSDT', '15m')
225 | print('plotting charts')
226 | c.plot_charts()
227 | print('done')
228 | sys.exit()
229 |
--------------------------------------------------------------------------------
/algo_trader.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 | import time
4 | from datetime import datetime, timedelta
5 |
6 | from apscheduler.schedulers.background import BackgroundScheduler
7 | from http.client import RemoteDisconnected
8 | from trader import Trader
9 | from coin_data import CoinData
10 | from signals import Signals
11 |
12 |
13 | logger = logging.getLogger(__name__)
14 | logger.setLevel(logging.DEBUG)
15 | handler = logging.StreamHandler(sys.stdout)
16 | handler.setLevel(logging.DEBUG)
17 | formatter = logging.Formatter('%(asctime)s - [ %(levelname)s ] - %(message)s')
18 | handler.setFormatter(formatter)
19 | logger.addHandler(handler)
20 |
21 |
22 | class AlgoTrader:
23 |
24 | """Check each coin for signals and make trades in certain conditions.
25 | Conditions:
26 | - 15m RSI oversold/overbought and RSI divergence, and 1h ema_50/ema_200 trend
27 | - 15m RSI oversold/overbought in last hour and macd crossing up"""
28 |
29 | data = CoinData()
30 | trader = Trader()
31 | scheduler = BackgroundScheduler()
32 |
33 | def __init__(self):
34 | self.signals_dict = {}
35 | self.trend_markers = {}
36 | self.rsi_markers = {}
37 | self.get_signals()
38 | self.check_emas()
39 | self.event_loop = None
40 | self.recent_alerts = []
41 | self.trader = Trader()
42 | self.trader.settings['sl'] = 0.005
43 | self.trader.settings['tp'] = 0.015
44 | self.trader.settings['qty'] = 0.01
45 | self.trader.settings['db'] = 0.2
46 |
47 | def get_signals(self):
48 | inadequate_symbols = []
49 | for symbol in self.data.symbols:
50 | try:
51 | self.signals_dict[symbol] = ( # Signals(symbol, '1m'),
52 | Signals(symbol, '15m'),
53 | Signals(symbol, '1h'),
54 | Signals(symbol, '4h'))
55 | except IndexError:
56 | inadequate_symbols.append(symbol)
57 | continue
58 | for symbol in inadequate_symbols:
59 | self.data.symbols.remove(symbol)
60 | return self.signals_dict
61 |
62 | def check_emas(self):
63 | for symbol in self.data.symbols:
64 | # signals_1m = self.signals_dict[symbol][0]
65 | signals_15m = self.signals_dict[symbol][0]
66 | signals_1h = self.signals_dict[symbol][1]
67 | signals_4h = self.signals_dict[symbol][2]
68 | h4 = True if signals_4h.df.ema_50.iloc[-1] > signals_4h.df.ema_200.iloc[-1] else False
69 | h1 = True if signals_1h.df.ema_50.iloc[-1] > signals_1h.df.ema_200.iloc[-1] else False
70 | m15 = True if signals_15m.df.ema_50.iloc[-1] > signals_15m.df.ema_200.iloc[-1] else False
71 | # m1 = True if signals_1m.df.ema_50.iloc[-1] > signals_1m.df.ema_200.iloc[-1] else False
72 | self.trend_markers[symbol] = (m15, h1, h4)
73 | return self.trend_markers
74 |
75 | def long_condition(self, open_positions, recent_alerts):
76 | for symbol in self.signals_dict.keys():
77 | if self.trend_markers[symbol][1] and self.trend_markers[symbol][2]:
78 | if self.signals_dict[symbol][0].rsi_div_dict['confirmed bullish divergence']:
79 | alert = f'LONG {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
80 | if symbol not in open_positions and symbol not in recent_alerts:
81 | self.trader.trade(symbol, True)
82 | with open('buys.txt', 'a') as f:
83 | f.write(alert + '\n')
84 | self.recent_alerts.append(alert)
85 | logger.info(alert)
86 |
87 | def short_condition(self, open_positions, recent_alerts):
88 | for symbol in self.signals_dict.keys():
89 | if not self.trend_markers[symbol][1] and not self.trend_markers[symbol][2]:
90 | if self.signals_dict[symbol][0].rsi_div_dict['confirmed bearish divergence']:
91 | alert = f'SHORT {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
92 | if symbol not in open_positions and symbol not in recent_alerts:
93 | self.trader.trade(symbol, False)
94 | with open('buys.txt', 'a') as f:
95 | f.write(alert)
96 | self.recent_alerts.append(alert)
97 | logger.info(alert)
98 |
99 | def purge_alerts(self):
100 | old_alerts = []
101 | for alert in self.recent_alerts:
102 | split_alert = alert.split(' ')
103 | if datetime.strptime(' '.join(split_alert[3:5]), '%Y-%m-%d %H:%M:%S') < datetime.now() - timedelta(minutes=45):
104 | old_alerts.append(alert)
105 | for alert in old_alerts:
106 | self.recent_alerts.remove(alert)
107 |
108 | def check_rsi_div(self, symbol):
109 | if self.signals_dict[symbol][0].rsi_div_dict['confirmed bearish divergence']:
110 | return False
111 | elif self.signals_dict[symbol][0].rsi_div_dict['confirmed bullish divergence']:
112 | return True
113 | else:
114 | return None
115 |
116 | def check_rsi_ob_os(self, symbol):
117 | if self.signals_dict[symbol][0].rsi_ob_os_dict['overbought']:
118 | return False
119 | elif self.signals_dict[symbol][0].rsi_ob_os_dict['oversold']:
120 | return True
121 | else:
122 | return None
123 |
124 | def check_trend(self, symbol):
125 | if self.trend_markers[symbol][1] and self.trend_markers[symbol][2]:
126 | return True
127 | elif not self.trend_markers[symbol][1] and not self.trend_markers[symbol][2]:
128 | return False
129 | else:
130 | return None
131 |
132 | def rsi_ob_os_trade(self):
133 | for symbol in self.signals_dict.keys():
134 | if self.check_trend(symbol) is True:
135 | if self.check_rsi_ob_os(symbol) is True:
136 | self.trader.trade(symbol, True)
137 | elif self.check_trend(symbol) is False:
138 | if self.check_rsi_ob_os(symbol) is False:
139 | self.trader.trade(symbol, False)
140 |
141 | def rsi_ob_os_marker(self):
142 | for symbol in self.signals_dict.keys():
143 | if self.check_trend(symbol) is True:
144 | if self.check_rsi_ob_os(symbol) is True:
145 | self.rsi_markers['symbol'] = (True, datetime.now())
146 | elif self.check_trend(symbol) is False:
147 | if self.check_rsi_ob_os(symbol) is False:
148 | self.rsi_markers['symbol'] = (False, datetime.now())
149 |
150 | def purge_rsi_markers(self):
151 | old_keys = []
152 | for key, value in self.rsi_markers.items():
153 | if value[1] < datetime.now() - timedelta(hours=1):
154 | old_keys.append(key)
155 | for key in old_keys:
156 | self.rsi_markers.pop(key, None)
157 |
158 | def rsi_div_trade(self, open_positions, recent_alerts):
159 | for symbol in self.signals_dict.keys():
160 | if symbol not in open_positions and symbol not in recent_alerts:
161 | if self.check_trend(symbol) is True:
162 | if self.check_rsi_div(symbol) is True:
163 | self.trader.trade(symbol, True)
164 | alert = f'LONGED {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
165 | self.handle_alert(alert)
166 | elif self.check_trend(symbol) is False:
167 | if self.check_rsi_div(symbol) is False:
168 | self.trader.trade(symbol, False)
169 | alert = f'SHORTED {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
170 | self.handle_alert(alert)
171 |
172 | def handle_alert(self, alert):
173 | self.recent_alerts.append(alert)
174 | logger.info(alert)
175 |
176 | def check_conditions(self):
177 | try:
178 | logger.debug('checking signal data')
179 | start_time = datetime.now()
180 |
181 | # Get new signals data
182 | self.get_signals()
183 | self.check_emas()
184 | self.purge_alerts()
185 | recent_alerts_symbols = [alert.split(' ')[1] for alert in self.recent_alerts]
186 | open_positions = self.trader.check_positions_cancel_open_orders()
187 |
188 | log_statement = 'took: {}'.format(datetime.now() - start_time)
189 | logger.debug(log_statement)
190 | logger.debug('checking trade conditions')
191 |
192 | # Check trade conditions
193 | self.rsi_div_trade(open_positions, recent_alerts_symbols)
194 |
195 | if self.recent_alerts:
196 | recent = ', '.join(self.recent_alerts)
197 | logger.debug(recent)
198 |
199 | total_time = datetime.now() - start_time
200 | log_statement = 'total_time: {}'.format(total_time)
201 | logger.debug(log_statement)
202 |
203 | except Exception as e:
204 | log_statement = f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}: {e}'
205 | logger.warning(log_statement)
206 |
207 | def save_data(self):
208 | self.data.save_latest_data()
209 |
210 | def schedule_tasks(self):
211 | self.scheduler.add_job(self.save_data, trigger='cron', minute='*/1', second="58")
212 | self.scheduler.add_job(self.check_conditions, trigger='cron', minute='*/1')
213 | self.scheduler.start()
214 |
215 | def stop_tasks(self):
216 | self.scheduler.remove_all_jobs()
217 | self.scheduler.shutdown()
218 |
219 | def loop(self):
220 | try:
221 | self.schedule_tasks()
222 | while True:
223 | time.sleep(1)
224 | except KeyboardInterrupt as e:
225 | self.stop_tasks()
226 | sys.exit()
227 |
228 |
229 | if __name__ == '__main__':
230 | at = AlgoTrader()
231 | at.loop()
232 |
--------------------------------------------------------------------------------
/async_signals.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | from datetime import datetime
4 |
5 | import pandas as pd
6 | import pandas_ta as ta
7 | import numpy as np
8 | import scipy.signal
9 | from trader import Trader
10 | from coin_data import CoinData
11 |
12 | client = Trader().client
13 | file_path = os.path.abspath(os.path.dirname(__file__))
14 | os.chdir(file_path)
15 |
16 |
17 | class Signals:
18 |
19 | def __init__(self, symbol, tf):
20 |
21 | """Check for signals for given symbol and timeframe"""
22 |
23 | self.symbol = symbol.upper()
24 | self.tf = tf
25 | self.df = CoinData.get_dataframe(symbol, tf)
26 | self.df = self.df_ta()
27 | # self.df = self.get_heiken_ashi()
28 | # self.vol_signal = self.vol_rise_fall()
29 | # self.vol_candle = self.large_vol_candle()
30 | # self.HA_trend = self.get_heiken_ashi_trend(self.get_heiken_ashi(self.df))
31 | self.rsi_ob_os_dict = {
32 | 'overbought': False,
33 | 'oversold': False,
34 | }
35 |
36 | self.rsi_div_dict = {
37 | 'possible bearish divergence': False,
38 | 'possible bullish divergence': False,
39 | 'confirmed bearish divergence': False,
40 | 'confirmed bullish divergence': False,
41 | }
42 |
43 | self.macd_dict = {
44 | 'MACD cross': None,
45 | 'MACD 0 cross': None,
46 | }
47 |
48 | self.ema_signals_dict = {
49 | 'Price crossing EMA200': None,
50 | 'EMA20 crossing EMA50': None,
51 | 'EMA50 crossing EMA200': None,
52 | }
53 |
54 | async def _async_init(self):
55 | task_0 = asyncio.create_task(self.rsi_overbought_oversold())
56 | task_1 = asyncio.create_task(self.rsi_divergence())
57 | task_2 = asyncio.create_task(self.macd_signals())
58 | task_3 = asyncio.create_task(self.ema_signals())
59 | await task_0
60 | await task_1
61 | await task_2
62 | await task_3
63 |
64 | def full_check(self):
65 | self.rsi_divergence()
66 | self.ema_signals()
67 | self.macd_signals()
68 | self.rsi_overbought_oversold()
69 | # self.vol_rise_fall()
70 | # self.large_vol_candle()
71 |
72 | def df_ta(self) -> pd.DataFrame:
73 | df = self.df
74 | df['rsi'] = ta.rsi(df.close, 14)
75 | df = pd.concat((df, ta.macd(df.close, 12, 26, 9)), axis=1)
76 | df['ema_20'], df['ema_50'] = ta.ema(df.close, 20), ta.ema(df.close, 50)
77 | if len(df) >= 288:
78 | df['ema_200'] = ta.ema(df.close, 200)
79 | else:
80 | df['ema_200'] = ta.ema(df.close, len(df.close) - 3)
81 | df = df.tail(88)
82 | return df
83 |
84 | @staticmethod
85 | def get_heiken_ashi(df):
86 | df['HA_Close'] = (df['open'] + df['high'] + df['low'] + df['close']) / 4
87 | idx = df.index.name
88 | df.reset_index(inplace=True)
89 |
90 | for i in range(0, len(df)):
91 | if i == 0:
92 | df.at[i, 'HA_Open'] = ((df._get_value(i, 'open') + df._get_value(i, 'close')) / 2)
93 | else:
94 | df.at[i, 'HA_Open'] = ((df._get_value(i - 1, 'HA_Open') + df._get_value(i - 1, 'HA_Close')) / 2)
95 |
96 | if idx:
97 | df.set_index(idx, inplace=True)
98 |
99 | df['HA_High'] = df[['HA_Open', 'HA_Close', 'high']].max(axis=1)
100 | df['HA_Low'] = df[['HA_Open', 'HA_Close', 'low']].min(axis=1)
101 |
102 | return df
103 |
104 | @staticmethod
105 | def get_heiken_ashi_trend(df):
106 | if df['HA_Close'].iloc[-1] > df['HA_Open'].iloc[-1]:
107 | if df['HA_Close'].iloc[-2] > df['HA_Open'].iloc[-2]:
108 | return True
109 | elif df['HA_Close'].iloc[-1] < df['HA_Open'].iloc[-1]:
110 | if df['HA_Close'].iloc[-2] < df['HA_Open'].iloc[-2]:
111 | return False
112 | else:
113 | return None
114 |
115 | def rsi_divergence(self):
116 | rsi_array = np.array(self.df['rsi'].tail(20).array)
117 | close_array = np.array(self.df['close'].tail(20).array)
118 | rsi_peaks, _ = scipy.signal.find_peaks(rsi_array)
119 | rsi_troughs, _ = scipy.signal.find_peaks(-rsi_array)
120 | original_index = len(close_array)
121 | indices = np.array([])
122 |
123 | # bearish divergence confirmed: rsi formed lower peak while price formed higher peak
124 | if 70 <= rsi_array[rsi_peaks[-2]] >= rsi_array[rsi_peaks[-1]] >= rsi_array[-2] >= rsi_array[-1]:
125 | if close_array[rsi_peaks[-2]] <= close_array[rsi_peaks[-1]]:
126 | close_array = np.array([close_array[rsi_peaks[-2]], close_array[rsi_peaks[-1]]])
127 | rsi_array = np.array([rsi_array[rsi_peaks[-2]], rsi_array[rsi_peaks[-1]]])
128 | indices = np.array([rsi_peaks[-2], rsi_peaks[-1]])
129 | self.rsi_div_dict['confirmed bearish divergence'] = True
130 |
131 | # possible bearish divergence: rsi forming lower peak while price forming higher peak
132 | elif 70 <= rsi_array[rsi_peaks[-1]] >= rsi_array[-2] > rsi_array[-1]:
133 | if close_array[rsi_peaks[-1]] <= close_array[-1]:
134 | close_array = np.array([close_array[rsi_peaks[-1]], close_array[-1]])
135 | rsi_array = np.array([rsi_array[rsi_peaks[-1]], rsi_array[-1]])
136 | indices = np.array([rsi_peaks[-1], original_index])
137 | self.rsi_div_dict['possible bearish divergence'] = True
138 |
139 | # bullish divergence confirmed: rsi formed higher trough while price formed lower trough
140 | elif 30 >= rsi_array[rsi_troughs[-2]] <= rsi_array[rsi_troughs[-1]] <= rsi_array[-2] <= rsi_array[-1]:
141 | if close_array[rsi_troughs[-2]] >= close_array[rsi_troughs[-1]]:
142 | close_array = np.array([close_array[rsi_troughs[-2]], close_array[rsi_troughs[-1]]])
143 | rsi_array = np.array([rsi_array[rsi_troughs[-2]], rsi_array[rsi_troughs[-1]]])
144 | indices = np.array([rsi_troughs[-2], rsi_troughs[-1]])
145 | self.rsi_div_dict['confirmed bullish divergence'] = True
146 |
147 | # possible bullish divergence: rsi forming higher trough while price forming lower trough
148 | elif 30 >= rsi_array[rsi_troughs[-1]] <= rsi_array[-2] < rsi_array[-1]:
149 | if close_array[rsi_troughs[-1]] >= close_array[-1]:
150 | close_array = np.array([close_array[rsi_troughs[-1]], close_array[-1]])
151 | rsi_array = np.array([rsi_array[rsi_troughs[-1]], rsi_array[-1]])
152 | indices = np.array([rsi_troughs[-1], original_index])
153 | self.rsi_div_dict['possible bullish divergence'] = True
154 |
155 | return self.rsi_div_dict, close_array, rsi_array, indices
156 |
157 | def rsi_overbought_oversold(self, o_s=30, o_b=70):
158 | rsi_array = self.df['rsi'].array
159 | if rsi_array[-3] <= o_s <= rsi_array[-2]:
160 | self.rsi_ob_os_dict['oversold'] = True
161 | elif rsi_array[-3] >= o_b >= rsi_array[-2]:
162 | self.rsi_ob_os_dict['overbought'] = True
163 | return self.rsi_ob_os_dict
164 |
165 | def macd_signals(self):
166 | if self.df['MACD_12_26_9'].array[-2] > self.df['MACDs_12_26_9'].array[-2]:
167 | if self.df['MACD_12_26_9'].array[-3] < self.df['MACDs_12_26_9'].array[-3]:
168 | self.macd_dict['MACD cross'] = True
169 | elif self.df['MACD_12_26_9'].array[-2] < self.df['MACDs_12_26_9'].array[-2]:
170 | if self.df['MACD_12_26_9'].array[-3] > self.df['MACDs_12_26_9'].array[-3]:
171 | self.macd_dict['MACD cross'] = False
172 | if (self.df['MACD_12_26_9'].array[-2], self.df['MACDs_12_26_9'].array[-2]) > (0, 0):
173 | if (self.df['MACD_12_26_9'].array[-3], self.df['MACDs_12_26_9'].array[-3]) <= (0, 0):
174 | self.macd_dict['MACD 0 cross'] = True
175 | elif (self.df['MACD_12_26_9'].array[-2], self.df['MACDs_12_26_9'].array[-2]) < (0, 0):
176 | if (self.df['MACD_12_26_9'].array[-3], self.df['MACDs_12_26_9'].array[-3]) >= (0, 0):
177 | self.macd_dict['MACD 0 cross'] = False
178 |
179 | def ema_signals(self):
180 | ema_200 = self.df['ema_200'].array[-3:]
181 | ema_50 = self.df['ema_50'].array[-3:]
182 | ema_20 = self.df['ema_20'].array[-3:]
183 | price = self.df['close'].array[-3:]
184 | if ema_200[0] > price[0] and ema_200[1] >= price[1] and ema_200[2] < price[2]:
185 | self.ema_signals_dict['Price crossing EMA200'] = True
186 | elif ema_200[0] < price[0] and ema_200[1] <= price[1] and ema_200[2] > price[2]:
187 | self.ema_signals_dict['Price crossing EMA200'] = False
188 | if ema_20[0] > ema_50[0] and ema_20[1] >= ema_50[1] and ema_20[2] < ema_50[2]:
189 | self.ema_signals_dict['EMA20 crossing EMA50'] = False
190 | elif ema_20[0] < ema_50[0] and ema_20[1] <= ema_50[1] and ema_20[2] > ema_50[2]:
191 | self.ema_signals_dict['EMA20 crossing EMA50'] = True
192 | if ema_50[0] > ema_200[0] and ema_50[1] >= ema_200[1] and ema_50[2] < ema_200[2]:
193 | self.ema_signals_dict['EMA50 crossing EMA200'] = False
194 | elif ema_50[0] < ema_200[0] and ema_50[1] <= ema_200[1] and ema_50[2] > ema_200[2]:
195 | self.ema_signals_dict['EMA50 crossing EMA200'] = True
196 | return self.ema_signals_dict
197 |
198 | # def vol_rise_fall(self):
199 | # recent_vol = self.df.volume.tail(3).array
200 | # self.vol_signal = True if recent_vol[0] < recent_vol[1] < recent_vol[2] else False
201 | # return self.vol_signal
202 | #
203 | # def large_vol_candle(self):
204 | # self.vol_candle = True if self.df.volume.array[-1] >= self.df.volume.tail(14).values.mean()*2 else False
205 | # return self.vol_candle
206 |
207 |
208 | async def create_signals_instance(symbol='BTCUSDT', tf='15m'):
209 | s = Signals(symbol, tf)
210 | await s._async_init()
211 | return s
212 |
213 |
214 | if __name__ == '__main__':
215 | x = datetime.now()
216 | df = CoinData.get_dataframe('BTCUSDT', '15m')
217 | print(Signals.get_heiken_ashi(df))
218 | print(datetime.now() - x)
--------------------------------------------------------------------------------
/signals.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime
3 |
4 | import pandas as pd
5 | import pandas_ta as ta
6 | import numpy as np
7 | import scipy.signal
8 | from trader import Trader
9 | from coin_data import CoinData
10 |
11 | client = Trader().client
12 | file_path = os.path.abspath(os.path.dirname(__file__))
13 | os.chdir(file_path)
14 |
15 |
16 | class Signals:
17 |
18 | def __init__(self, symbol='BTCUSDT', tf='4h'):
19 |
20 | """Check for signals for given symbol and timeframe"""
21 |
22 | self.symbol = symbol.upper()
23 | self.tf = tf
24 | self.df = CoinData.get_dataframe(symbol, tf)
25 | self.df = self.df_ta()
26 | # self.df = self.get_heiken_ashi()
27 | self.vol_signal = self.vol_rise_fall()
28 | self.vol_candle = self.large_vol_candle()
29 | # self.HA_trend = self.get_heiken_ashi_trend(self.get_heiken_ashi(self.df))
30 | self.rsi_ob_os_dict = {
31 | 'overbought': False,
32 | 'oversold': False,
33 | }
34 | self.rsi_overbought_oversold()
35 | self.rsi_div_dict = {
36 | 'possible bearish divergence': False,
37 | 'possible bullish divergence': False,
38 | 'confirmed bearish divergence': False,
39 | 'confirmed bullish divergence': False,
40 | }
41 | self.rsi_divergence()
42 | self.macd_dict = {
43 | 'MACD cross': None,
44 | 'MACD 0 cross': None,
45 | }
46 | self.macd_signals()
47 | self.ema_signals_dict = {
48 | 'Price crossing EMA200': None,
49 | 'EMA20 crossing EMA50': None,
50 | 'EMA50 crossing EMA200': None,
51 | }
52 | self.ema_signals()
53 |
54 | def full_check(self):
55 | self.rsi_divergence()
56 | self.ema_signals()
57 | self.macd_signals()
58 | self.rsi_overbought_oversold()
59 | self.vol_rise_fall()
60 | self.large_vol_candle()
61 |
62 | def df_ta(self) -> pd.DataFrame:
63 | df = self.df
64 | df['rsi'] = ta.rsi(df.close, 14)
65 | df = pd.concat((df, ta.macd(df.close, 12, 26, 9)), axis=1)
66 | df['ema_20'], df['ema_50'] = ta.ema(df.close, 20), ta.ema(df.close, 50)
67 | if len(df) >= 288:
68 | df['ema_200'] = ta.ema(df.close, 200)
69 | else:
70 | df['ema_200'] = ta.ema(df.close, len(df.close) - 3)
71 | df = df.tail(88)
72 | return df
73 |
74 | @staticmethod
75 | def get_heiken_ashi(df):
76 | df['HA_Close'] = (df['open'] + df['high'] + df['low'] + df['close']) / 4
77 | idx = df.index.name
78 | df.reset_index(inplace=True)
79 |
80 | for i in range(0, len(df)):
81 | if i == 0:
82 | df.at[i, 'HA_Open'] = ((df._get_value(i, 'open') + df._get_value(i, 'close')) / 2)
83 | else:
84 | df.at[i, 'HA_Open'] = ((df._get_value(i - 1, 'HA_Open') + df._get_value(i - 1, 'HA_Close')) / 2)
85 |
86 | if idx:
87 | df.set_index(idx, inplace=True)
88 |
89 | df['HA_High'] = df[['HA_Open', 'HA_Close', 'high']].max(axis=1)
90 | df['HA_Low'] = df[['HA_Open', 'HA_Close', 'low']].min(axis=1)
91 |
92 | return df
93 |
94 | @staticmethod
95 | def get_heiken_ashi_trend(df):
96 | if df['HA_Close'].iloc[-1] > df['HA_Open'].iloc[-1]:
97 | if df['HA_Close'].iloc[-2] > df['HA_Open'].iloc[-2] or all([df['HA_Low'].iloc[-2] < df['HA_Open'].iloc[-2],
98 | df['HA_High'].iloc[-2] > df['HA_Close'].iloc[-2]]):
99 | return True
100 | elif df['HA_Close'].iloc[-1] < df['HA_Open'].iloc[-1]:
101 | if df['HA_Close'].iloc[-2] < df['HA_Open'].iloc[-2] or all([df['HA_High'].iloc[-2] > df['HA_Open'].iloc[-2],
102 | df['HA_Low'].iloc[-2] < df['HA_Close'].iloc[-2]]):
103 | return False
104 | else:
105 | return None
106 |
107 | def rsi_divergence(self):
108 | rsi_array = np.array(self.df['rsi'].tail(20).array)
109 | close_array = np.array(self.df['close'].tail(20).array)
110 | rsi_peaks, _ = scipy.signal.find_peaks(rsi_array)
111 | rsi_troughs, _ = scipy.signal.find_peaks(-rsi_array)
112 | original_index = len(close_array)
113 | indices = np.array([])
114 |
115 | # bearish divergence confirmed: rsi formed lower peak while price formed higher peak
116 | if 70 <= rsi_array[rsi_peaks[-2]] >= rsi_array[rsi_peaks[-1]] >= rsi_array[-2] >= rsi_array[-1]:
117 | if close_array[rsi_peaks[-2]] <= close_array[rsi_peaks[-1]]:
118 | close_array = np.array([close_array[rsi_peaks[-2]], close_array[rsi_peaks[-1]]])
119 | rsi_array = np.array([rsi_array[rsi_peaks[-2]], rsi_array[rsi_peaks[-1]]])
120 | indices = np.array([rsi_peaks[-2], rsi_peaks[-1]])
121 | self.rsi_div_dict['confirmed bearish divergence'] = True
122 |
123 | # possible bearish divergence: rsi forming lower peak while price forming higher peak
124 | elif 70 <= rsi_array[rsi_peaks[-1]] >= rsi_array[-2] > rsi_array[-1]:
125 | if close_array[rsi_peaks[-1]] <= close_array[-1]:
126 | close_array = np.array([close_array[rsi_peaks[-1]], close_array[-1]])
127 | rsi_array = np.array([rsi_array[rsi_peaks[-1]], rsi_array[-1]])
128 | indices = np.array([rsi_peaks[-1], original_index])
129 | self.rsi_div_dict['possible bearish divergence'] = True
130 |
131 | # bullish divergence confirmed: rsi formed higher trough while price formed lower trough
132 | elif 30 >= rsi_array[rsi_troughs[-2]] <= rsi_array[rsi_troughs[-1]] <= rsi_array[-2] <= rsi_array[-1]:
133 | if close_array[rsi_troughs[-2]] >= close_array[rsi_troughs[-1]]:
134 | close_array = np.array([close_array[rsi_troughs[-2]], close_array[rsi_troughs[-1]]])
135 | rsi_array = np.array([rsi_array[rsi_troughs[-2]], rsi_array[rsi_troughs[-1]]])
136 | indices = np.array([rsi_troughs[-2], rsi_troughs[-1]])
137 | self.rsi_div_dict['confirmed bullish divergence'] = True
138 |
139 | # possible bullish divergence: rsi forming higher trough while price forming lower trough
140 | elif 30 >= rsi_array[rsi_troughs[-1]] <= rsi_array[-2] < rsi_array[-1]:
141 | if close_array[rsi_troughs[-1]] >= close_array[-1]:
142 | close_array = np.array([close_array[rsi_troughs[-1]], close_array[-1]])
143 | rsi_array = np.array([rsi_array[rsi_troughs[-1]], rsi_array[-1]])
144 | indices = np.array([rsi_troughs[-1], original_index])
145 | self.rsi_div_dict['possible bullish divergence'] = True
146 |
147 | return self.rsi_div_dict, close_array, rsi_array, indices
148 |
149 | def rsi_overbought_oversold(self, o_s=30, o_b=70):
150 | rsi_array = self.df['rsi'].array
151 | if rsi_array[-3] <= o_s <= rsi_array[-2]:
152 | self.rsi_ob_os_dict['oversold'] = True
153 | elif rsi_array[-3] >= o_b >= rsi_array[-2]:
154 | self.rsi_ob_os_dict['overbought'] = True
155 | return self.rsi_ob_os_dict
156 |
157 | def macd_signals(self):
158 | if self.df['MACD_12_26_9'].array[-2] > self.df['MACDs_12_26_9'].array[-2]:
159 | if self.df['MACD_12_26_9'].array[-3] < self.df['MACDs_12_26_9'].array[-3]:
160 | self.macd_dict['MACD cross'] = True
161 | elif self.df['MACD_12_26_9'].array[-2] < self.df['MACDs_12_26_9'].array[-2]:
162 | if self.df['MACD_12_26_9'].array[-3] > self.df['MACDs_12_26_9'].array[-3]:
163 | self.macd_dict['MACD cross'] = False
164 | if (self.df['MACD_12_26_9'].array[-2], self.df['MACDs_12_26_9'].array[-2]) > (0, 0):
165 | if (self.df['MACD_12_26_9'].array[-3], self.df['MACDs_12_26_9'].array[-3]) <= (0, 0):
166 | self.macd_dict['MACD 0 cross'] = True
167 | elif (self.df['MACD_12_26_9'].array[-2], self.df['MACDs_12_26_9'].array[-2]) < (0, 0):
168 | if (self.df['MACD_12_26_9'].array[-3], self.df['MACDs_12_26_9'].array[-3]) >= (0, 0):
169 | self.macd_dict['MACD 0 cross'] = False
170 | if self.df['MACD_12_26_9'].array[-1] > self.df['MACDs_12_26_9'].array[-1]:
171 | self.macd_dict['MACD up'] = True
172 | else:
173 | self.macd_dict['MACD up'] = False
174 |
175 | def ema_signals(self):
176 | ema_200 = self.df['ema_200'].array[-3:]
177 | ema_50 = self.df['ema_50'].array[-3:]
178 | ema_20 = self.df['ema_20'].array[-3:]
179 | price = self.df['close'].array[-3:]
180 | if ema_200[0] > price[0] and ema_200[1] >= price[1] and ema_200[2] < price[2]:
181 | self.ema_signals_dict['Price crossing EMA200'] = True
182 | elif ema_200[0] < price[0] and ema_200[1] <= price[1] and ema_200[2] > price[2]:
183 | self.ema_signals_dict['Price crossing EMA200'] = False
184 | if ema_20[0] > ema_50[0] and ema_20[1] >= ema_50[1] and ema_20[2] < ema_50[2]:
185 | self.ema_signals_dict['EMA20 crossing EMA50'] = False
186 | elif ema_20[0] < ema_50[0] and ema_20[1] <= ema_50[1] and ema_20[2] > ema_50[2]:
187 | self.ema_signals_dict['EMA20 crossing EMA50'] = True
188 | if ema_50[0] > ema_200[0] and ema_50[1] >= ema_200[1] and ema_50[2] < ema_200[2]:
189 | self.ema_signals_dict['EMA50 crossing EMA200'] = False
190 | elif ema_50[0] < ema_200[0] and ema_50[1] <= ema_200[1] and ema_50[2] > ema_200[2]:
191 | self.ema_signals_dict['EMA50 crossing EMA200'] = True
192 | return self.ema_signals_dict
193 |
194 | def vol_rise_fall(self):
195 | recent_vol = self.df.volume.tail(3).array
196 | self.vol_signal = True if recent_vol[0] < recent_vol[1] < recent_vol[2] else False
197 | return self.vol_signal
198 |
199 | def large_vol_candle(self):
200 | self.vol_candle = True if self.df.volume.array[-1] >= self.df.volume.tail(14).values.mean()*2 else False
201 | return self.vol_candle
202 |
203 | if __name__ == '__main__':
204 | x = datetime.now()
205 | df = CoinData.get_dataframe('BTCUSDT', '15m')
206 | print(Signals.get_heiken_ashi(df))
207 | print(datetime.now() - x)
--------------------------------------------------------------------------------
/coin_data.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sqlite3
3 | import time
4 | from datetime import datetime, timedelta
5 | from threading import Thread
6 |
7 | import pandas as pd
8 | import numpy as np
9 | import scipy.signal
10 | from binance.websockets import BinanceSocketManager
11 | from twisted.internet import reactor
12 |
13 | from trader import Trader
14 | from utils import get_popular_coins
15 |
16 | client = Trader().client
17 | file_path = os.path.abspath(os.path.dirname(__file__))
18 | os.chdir(file_path)
19 |
20 | data_path = os.path.dirname(os.path.abspath(__file__))
21 | data_path = os.path.join(data_path, 'data')
22 |
23 |
24 | NUMBER_OF_SYMBOLS = 50
25 |
26 |
27 | class CoinData:
28 | def __init__(self):
29 | """Get most popular symbols, download historical data, start live data web socket"""
30 | # os.system('cls' if os.name == 'nt' else 'clear')
31 | print('Getting symbol list')
32 | self.symbols = get_popular_coins() # [:NUMBER_OF_SYMBOLS]
33 | self.bad_symbols = []
34 | self.intervals = ['1m', '15m', '1h', '4h'] # '1m',
35 | self.latest_klines = {}
36 | self.data_dict = {}
37 | for s in self.symbols:
38 | self.data_dict[s] = {}
39 | self.latest_klines[s] = {}
40 | for interval in self.intervals:
41 | self.data_dict[s][interval] = []
42 | self.latest_klines[s][interval] = {}
43 |
44 | self.tf_dict = {
45 | '1m': 1,
46 | '15m': 15,
47 | '1h': 60,
48 | '4h': 240,
49 | }
50 | self.bsm = BinanceSocketManager(client)
51 | self.conn_key = self.bsm.start_multiplex_socket(self.get_streams(), self.get_data)
52 | self.shutdown = False
53 | self.t = Thread(target=self.websocket_loop)
54 | self.t.setDaemon(True)
55 | self.t.start()
56 | self.create_database()
57 | self.adjust_symbols()
58 | print('Coin data initialized')
59 | self.most_volatile_symbols = self.return_most_volatile()
60 |
61 | def adjust_symbols(self):
62 | for symbol in self.bad_symbols:
63 | self.symbols.remove(symbol)
64 |
65 | def get_data(self, msg):
66 | static = msg
67 | if not self.latest_klines[static['data']['k']['s']][static['data']['k']['i']]:
68 | self.latest_klines[static['data']['k']['s']][static['data']['k']['i']] = static['data']['k']
69 | elif datetime.fromtimestamp(static['data']['k']['t']/1000) >= datetime.fromtimestamp(self.latest_klines[static['data']['k']['s']][static['data']['k']['i']]['t']/1000):
70 | self.latest_klines[static['data']['k']['s']][static['data']['k']['i']] = static['data']['k']
71 | else:
72 | print('______WARNING______')
73 | print('caught irrelevant timestamp: ' + datetime.fromtimestamp(static['data']['k']['t']/1000).strftime('%H:%M:%S'))
74 | print(static['data']['k']['s'] + static['data']['k']['i'])
75 |
76 | @staticmethod
77 | def get_dataframe(symbol, interval):
78 | if not symbol[0].isalpha():
79 | symbol = symbol[1:]
80 | conn = sqlite3.connect('symbols.db')
81 | try:
82 | df = pd.read_sql_query(f'SELECT * FROM {symbol}_{interval}', conn)
83 | df.date = pd.to_datetime(df.date)
84 | df.set_index('date', inplace=True)
85 | df.rename_axis('date', inplace=True)
86 | finally:
87 | conn.close()
88 | return df
89 |
90 | @staticmethod
91 | def volatility(df):
92 | df = df.tail(16)
93 | close_array = np.array(df['close'].tail(40).array)
94 | peaks, _peaks = scipy.signal.find_peaks(close_array)
95 | troughs, _troughs = scipy.signal.find_peaks(-close_array)
96 | if close_array[peaks][0] < close_array[peaks][-1]:
97 | divisor = max(close_array[peaks])
98 | else:
99 | divisor = min(close_array[troughs])
100 | return (max(close_array[peaks]) - min(close_array[troughs])) / divisor
101 |
102 | def return_most_volatile(self, n=10):
103 | volatities = []
104 | for symbol in self.symbols:
105 |
106 | volatities.append((symbol, self.volatility(self.get_dataframe(symbol, '15m'))))
107 | volatities = sorted(volatities, key=lambda x: x[1], reverse=True)
108 | return volatities[:n]
109 |
110 | def create_database(self):
111 | if os.path.isfile('symbols.db'):
112 | conn = sqlite3.connect('symbols.db')
113 | cursor = conn.cursor()
114 | tabs = {tab[0] for tab in cursor.execute("select name from sqlite_master where type = 'table'").fetchall()}
115 | if tabs:
116 | time = cursor.execute('SELECT MAX(date) FROM BTCUSDT_15m').fetchone()[0]
117 | if time and datetime.strptime(time, '%Y-%m-%d %H:%M:%S') >= datetime.now() - timedelta(minutes=30):
118 | for symbol in self.symbols:
119 | if self.check_symbol(symbol + '_15m') in tabs:
120 | continue
121 | else:
122 | print(f'{symbol} not in tabs')
123 | os.remove('symbols.db')
124 | self.create_database()
125 | break
126 | return
127 | else:
128 | print(f'{time} is too old')
129 | os.remove('symbols.db')
130 | self.create_database()
131 | else:
132 | print('no tables in database')
133 | os.remove('symbols.db')
134 | self.create_database()
135 | else:
136 | print('recreating database from scratch')
137 | conn = sqlite3.connect('symbols.db')
138 | cursor = conn.cursor()
139 | try:
140 | for symbol in self.symbols:
141 | for interval in self.intervals:
142 | safe_symbol = self.check_symbol(symbol)
143 | query = f'CREATE TABLE {safe_symbol}_{interval} ' \
144 | f'(date datetime, open dec(6, 8), ' \
145 | f'high dec(6, 8), low dec(6, ' \
146 | f'8), close dec(' \
147 | f'6, 8), volume dec(12, 2))'
148 | try:
149 | cursor.execute(query)
150 | except sqlite3.OperationalError as e:
151 | if str(e)[-6:] == 'exists':
152 | continue
153 | else:
154 | raise e
155 | print('created tables for ' + ', '.join(self.symbols))
156 | self.save_original_data()
157 | finally:
158 | conn.commit()
159 | conn.close()
160 |
161 | def check_symbol(self, symbol):
162 | safe_symbol = symbol
163 | if not symbol[0].isalpha():
164 | safe_symbol = symbol[1:]
165 | self.check_symbol(safe_symbol)
166 | return safe_symbol
167 |
168 | def save_original_data(self):
169 | print('Downloading historical data')
170 | conn = sqlite3.connect('symbols.db')
171 | cursor = conn.cursor()
172 | try:
173 | for symbol in self.symbols:
174 | for interval in self.intervals:
175 | data = client.futures_klines(symbol=symbol, interval=interval, requests_params={'timeout': 20})
176 | if len(data) < 200:
177 | safe_symbol = self.check_symbol(symbol)
178 | query = f'DROP TABLE {safe_symbol}_{interval}'
179 | cursor.execute(query)
180 | print(f'Dropped {symbol} (not enough data)')
181 | self.bad_symbols.append(symbol)
182 | break
183 | else:
184 | for kline in data:
185 | row = [datetime.fromtimestamp(kline[0] / 1000),
186 | float(kline[1]),
187 | float(kline[2]),
188 | float(kline[3]),
189 | float(kline[4]),
190 | float(kline[7])]
191 | safe_symbol = self.check_symbol(symbol)
192 | query = f'''INSERT INTO {safe_symbol}_{interval} VALUES
193 | ("{row[0]}", {row[1]}, {row[2]}, {row[3]}, {row[4]}, {row[5]})'''
194 | cursor.execute(query)
195 | finally:
196 | conn.commit()
197 | conn.close()
198 |
199 | def save_latest_data(self):
200 | conn = sqlite3.connect('symbols.db')
201 | cursor = conn.cursor()
202 | try:
203 | for symbol in self.symbols:
204 | for interval in self.intervals:
205 | if self.latest_klines[symbol][interval]:
206 | new_row = [datetime.fromtimestamp(self.latest_klines[symbol][interval]['t'] / 1000),
207 | self.latest_klines[symbol][interval]['o'],
208 | self.latest_klines[symbol][interval]['h'],
209 | self.latest_klines[symbol][interval]['l'],
210 | self.latest_klines[symbol][interval]['c'],
211 | self.latest_klines[symbol][interval]['q']]
212 | safe_symbol = self.check_symbol(symbol)
213 | date_query= f'SELECT MAX(date) FROM {safe_symbol}_{interval}'
214 | old_row_date = cursor.execute(date_query).fetchone()[0]
215 | if str(old_row_date) == str(new_row[0]):
216 | query = f'UPDATE {safe_symbol}_{interval} SET open = {new_row[1]}, high = {new_row[2]}, low = {new_row[3]}, close = {new_row[4]}, volume = {new_row[5]} WHERE date = (SELECT MAX(date) FROM {safe_symbol}_{interval})'
217 | else:
218 | query = f'''INSERT INTO {safe_symbol}_{interval} VALUES
219 | ("{new_row[0]}", {new_row[1]}, {new_row[2]}, {new_row[3]}, {new_row[4]}, {new_row[5]})'''
220 | cursor.execute(query)
221 | finally:
222 | conn.commit()
223 | conn.close()
224 |
225 | def get_streams(self):
226 | streams = []
227 | for symbol in self.symbols:
228 | for interval in self.intervals:
229 | streams += [f'{symbol.lower()}@kline_{interval.lower()}']
230 | return streams
231 |
232 | def websocket_loop(self):
233 | try:
234 | self.bsm.start()
235 | while True:
236 | time.sleep(1)
237 | except Exception as e:
238 | print(e)
239 | finally:
240 | self.bsm_tear_down()
241 |
242 | def bsm_tear_down(self):
243 | self.bsm.stop_socket(self.conn_key)
244 | reactor.stop()
245 | print('bsm tear down success')
246 |
247 |
248 | if __name__ == "__main__":
249 | c = CoinData()
250 | while True:
251 | c.save_latest_data()
252 | time.sleep(5)
253 |
--------------------------------------------------------------------------------
/analysis/chart.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import datetime
3 | import io
4 | import os
5 | import sys
6 |
7 | import matplotlib
8 |
9 | matplotlib.use('agg')
10 |
11 | import mplfinance as mpf
12 | import pandas as pd
13 | import pandas_ta as ta
14 |
15 | from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
16 |
17 | from matplotlib import pyplot as plt
18 |
19 | from coin_data import CoinData
20 |
21 | plt.style.use('dark_background')
22 | import numpy as np
23 | import numba as nb
24 | import csv
25 |
26 | from binance.client import Client
27 |
28 | client = Client(os.getenv('bbot_pub'), os.getenv('bbot_sec'))
29 |
30 | data_dict = {}
31 |
32 | def read_data():
33 | with open('recent_trades.csv', newline='') as f:
34 | trade_reader = csv.reader(f, delimiter=' ', quotechar='|')
35 | for row in trade_reader:
36 | if row:
37 | # print()
38 | if row[1] not in data_dict.keys():
39 | data_dict[row[1]] = [[row[0]] + row[2:]]
40 | else:
41 | data_dict[row[1]].append([row[0]] + row[2:])
42 | return data_dict
43 |
44 |
45 | @nb.jit(fastmath=True, nopython=True)
46 | def calc_rsi( array, deltas, avg_gain, avg_loss, n ):
47 |
48 | # Use Wilder smoothing method
49 | up = lambda x: x if x > 0 else 0
50 | down = lambda x: -x if x < 0 else 0
51 | i = n+1
52 | for d in deltas[n+1:]:
53 | avg_gain = ((avg_gain * (n-1)) + up(d)) / n
54 | avg_loss = ((avg_loss * (n-1)) + down(d)) / n
55 | if avg_loss != 0:
56 | rs = avg_gain / avg_loss
57 | array[i] = 100 - (100 / (1 + rs))
58 | else:
59 | array[i] = 100
60 | i += 1
61 |
62 | return array
63 |
64 | def get_rsi( array, n = 14 ):
65 |
66 | deltas = np.append([0],np.diff(array))
67 |
68 | avg_gain = np.sum(deltas[1:n+1].clip(min=0)) / n
69 | avg_loss = -np.sum(deltas[1:n+1].clip(max=0)) / n
70 |
71 | array = np.empty(deltas.shape[0])
72 | array.fill(np.nan)
73 |
74 | array = calc_rsi( array, deltas, avg_gain, avg_loss, n )
75 | return array
76 |
77 | class Charts:
78 |
79 | def __init__(self, symbol='BTCUSDT', tf='1h', entry=None, direction=None):
80 | """When initialised, creates chart for given symbol and timeframe
81 | :param symbol: str uppercase eg. 'BTCUSDT'
82 | :param tf: str lowercase eg. '1m', '15m', '1h', '4h'
83 | :param entry: float if open positions
84 | :param direction: str 'LONG', 'SHORT' or None depending on position"""
85 | self.symbol = symbol.upper()
86 | self.tf = tf
87 | self.df = CoinData.get_dataframe(self.symbol, self.tf)
88 | self.df = self.df_ta()
89 | self.entry = entry
90 | self.direction = True if direction == 'LONG' else False
91 | self.tp = 0.03
92 | self.sl = 0.01
93 |
94 | def df_ta(self) -> pd.DataFrame:
95 | df = self.df
96 | # df['rsi'] = ta.rsi(df.close, 14)
97 | df['rsi'] = get_rsi(df.close, 14)
98 | df = pd.concat((df, ta.macd(df.close, 12, 26, 9)), axis=1)
99 | df['ema_20'], df['ema_50'] = ta.ema(df.close, 20), ta.ema(df.close, 50)
100 | if len(df) >= 288:
101 | df['ema_200'] = ta.ema(df.close, 200)
102 | else:
103 | df['ema_200'] = ta.ema(df.close, len(df.close) - 3)
104 | df = df.tail(88)
105 | return df
106 |
107 | def trades_series(self, symbol):
108 | d = read_data()
109 | for key in d.keys():
110 | if key == symbol:
111 | trades = [{'datetime': datetime.datetime.strptime(d[symbol][i][0], "%Y-%m-%d %H:%M:%S.%f"), 'price': d[symbol][i][2]} for i in range(len(d[symbol]))]
112 | df = pd.DataFrame(trades)
113 | df.set_index('datetime', inplace=True)
114 | df.rename_axis('date', inplace=True)
115 | return df
116 |
117 | @staticmethod
118 | def get_rsi_timeseries(prices, n=14):
119 | # RSI = 100 - (100 / (1 + RS))
120 | # where RS = (Wilder-smoothed n-period average of gains / Wilder-smoothed n-period average of -losses)
121 | # Note that losses above should be positive values
122 | # Wilder-smoothing = ((previous smoothed avg * (n-1)) + current value to average) / n
123 | # For the very first "previous smoothed avg" (aka the seed value), we start with a straight average.
124 | # Therefore, our first RSI value will be for the n+2nd period:
125 | # 0: first delta is nan
126 | # 1:
127 | # ...
128 | # n: lookback period for first Wilder smoothing seed value
129 | # n+1: first RSI
130 |
131 | # First, calculate the gain or loss from one price to the next. The first value is nan so replace with 0.
132 | deltas = (prices - prices.shift(1)).fillna(0)
133 |
134 | # Calculate the straight average seed values.
135 | # The first delta is always zero, so we will use a slice of the first n deltas starting at 1,
136 | # and filter only deltas > 0 to get gains and deltas < 0 to get losses
137 | avg_of_gains = deltas[1:n + 1][deltas > 0].sum() / n
138 | avg_of_losses = -deltas[1:n + 1][deltas < 0].sum() / n
139 |
140 | # Set up pd.Series container for RSI values
141 | rsi_series = pd.Series(0.0, deltas.index)
142 |
143 | # Now calculate RSI using the Wilder smoothing method, starting with n+1 delta.
144 | up = lambda x: x if x > 0 else 0
145 | down = lambda x: -x if x < 0 else 0
146 | i = n + 1
147 | for d in deltas[n + 1:]:
148 | avg_of_gains = ((avg_of_gains * (n - 1)) + up(d)) / n
149 | avg_of_losses = ((avg_of_losses * (n - 1)) + down(d)) / n
150 | if avg_of_losses != 0:
151 | rs = avg_of_gains / avg_of_losses
152 | rsi_series[i] = 100 - (100 / (1 + rs))
153 | else:
154 | rsi_series[i] = 100
155 | i += 1
156 |
157 | return rsi_series
158 |
159 |
160 | def main_chart(self):
161 | fig, axes = plt.subplots(nrows=3, ncols=1, gridspec_kw={'height_ratios': [3, 1, 1]})
162 | fig.suptitle(f"{self.symbol} {self.tf}", fontsize=16)
163 | ax_r = axes[0].twinx()
164 | mc = mpf.make_marketcolors(up='#00e600', down='#ff0066',
165 | edge={'up': '#00e600', 'down': '#ff0066'},
166 | wick={'up': '#00e600', 'down': '#ff0066'},
167 | volume={'up': '#808080', 'down': '#4d4d4d'},
168 | ohlc='black')
169 | s = mpf.make_mpf_style(marketcolors=mc)
170 | ax_r.set_alpha(0.01)
171 | axes[0].set_zorder(2)
172 | for ax in axes:
173 | ax.set_facecolor((0, 0, 0, 0))
174 | ax_r.set_zorder(1)
175 |
176 | axes[1].set_ylabel('RSI')
177 | axes[1].margins(x=0, y=0.1)
178 | axes[0].margins(x=0, y=0.05)
179 | axes[2].set_ylabel('MACD')
180 | ax_r.set_ylabel('')
181 | ax_r.yaxis.set_visible(False)
182 | axes[2].margins(0, 0.05)
183 | axes[0].xaxis.set_visible(False)
184 | axes[1].xaxis.set_visible(False)
185 |
186 | axes[0].yaxis.tick_left()
187 | axes[0].yaxis.set_label_position('right')
188 | axes[1].yaxis.set_label_position('right')
189 | axes[2].yaxis.set_label_position('right')
190 | plt.tight_layout()
191 | fig.autofmt_xdate()
192 | self.df.volume = self.df.volume.div(2)
193 | addplot_200 = mpf.make_addplot(self.df['ema_200'], type='line', ax=axes[0], width=1, color='#ff0066')
194 | addplot_50 = mpf.make_addplot(self.df['ema_50'], type='line', ax=axes[0], width=1, color='#00e600')
195 | addplot_trades = mpf.make_addplot(self.trades_series(self.symbol), type='scatter', ax=axes[0], width=5, color='#fff')
196 | mpf.plot(self.df, ax=axes[0], type="candle", style=s, volume=ax_r, ylabel='', addplot=[addplot_200, addplot_50])
197 | max_vol = max({y for index, y in self.df.volume.items()})
198 | ax_r.axis(ymin=0, ymax=max_vol * 3)
199 | self.df['rsi'].plot(ax=axes[1], legend=False, use_index=True, sharex=axes[0], color='#00e600')
200 | self.df['MACD_12_26_9'].plot(ax=axes[2], legend=False, use_index=True, sharex=axes[0], color='#00e600')
201 | self.df['MACDs_12_26_9'].plot(ax=axes[2], legend=False, use_index=True, sharex=axes[0], color='#ff0066')
202 | axes[2].axhline(0, color='gray', ls='--', linewidth=1)
203 | axes[1].axhline(70, color='gray', ls='--', linewidth=1)
204 | axes[1].axhline(30, color='gray', ls='--', linewidth=1)
205 | if self.entry:
206 | tp = self.entry + self.entry * self.tp if self.direction else self.entry - self.entry * self.tp
207 | sl = self.entry - self.entry * self.sl if self.direction else self.entry + self.entry * self.sl
208 | tp_color = 'red' if self.direction else 'green'
209 | sl_color = 'red' if not self.direction else 'green'
210 | axes[0].axhline(self.entry, color='yellow', ls="--", linewidth=.5)
211 | axes[0].axhline(tp, color=tp_color, ls="--", linewidth=.5)
212 | axes[0].axhline(sl, color=sl_color, ls="--", linewidth=.5)
213 | axes[2].set_xlabel('')
214 | img = io.BytesIO()
215 | FigureCanvas(fig).print_png(img)
216 | plot_url = base64.b64encode(img.getvalue()).decode()
217 | fig.savefig('plot.png', format='png')
218 | plt.close(fig)
219 | return plot_url
220 |
221 | # def plot_rsi_div(self):
222 | # rsi_array = np.array(self.df['rsi'].tail(20).array)
223 | # close_array = np.array(self.df['close'].tail(20).array)
224 | # rsi_peaks, _ = scipy.signal.find_peaks(rsi_array)
225 | # rsi_troughs, _ = scipy.signal.find_peaks(-rsi_array)
226 | # fig, (ax1, ax2) = plt.subplots(2, sharex=True)
227 | # fig.suptitle(f'{self.symbol} RSI Divergence {self.tf}')
228 | # ax1.set_ylabel('Close')
229 | # ax2.set_ylabel('RSI')
230 | # ax2.axhline(70, color='gray', ls='--')
231 | # ax2.axhline(30, color='gray', ls='--')
232 | # ax1.xaxis.set_visible(False)
233 | # ax2.xaxis.set_visible(False)
234 | # ax1.plot(close_array)
235 | # ax2.plot(rsi_array, color='green')
236 | # ax1.plot(rsi_peaks, close_array[rsi_peaks], '.', color="#ff0066")
237 | # ax2.plot(rsi_peaks, rsi_array[rsi_peaks], '.', color="#ff0066")
238 | # ax1.plot(rsi_troughs, close_array[rsi_troughs], '.', color="#00e600")
239 | # ax2.plot(rsi_troughs, rsi_array[rsi_troughs], '.', color="#00e600")
240 | # _, new_close_array, new_rsi_array, indices = self.rsi_divergence()
241 | # if len(close_array) != len(new_close_array):
242 | # ax1.plot(indices, new_close_array, color="#ff0066")
243 | # ax2.plot(indices, new_rsi_array, color="#ff0066")
244 | # img = io.BytesIO()
245 | # fig.savefig(img, format='png')
246 | # img.seek(0)
247 | # plot_url = base64.b64encode(img.getvalue()).decode()
248 | # plt.close()
249 | # return plot_url
250 |
251 | def plot_charts(self):
252 | self.main_chart()
253 | # self.plot_rsi_div()
254 |
255 |
256 | if __name__ == '__main__':
257 | c = Charts('SNXUSDT', '15m')
258 | print('plotting charts')
259 | c.plot_charts()
260 | print('done')
261 | sys.exit()
262 | # print(Charts('SNXUSDT', '15m').trades_series('SNXUSDT'))
263 |
--------------------------------------------------------------------------------
/trader.py:
--------------------------------------------------------------------------------
1 | import csv
2 | import http
3 | import json
4 | import logging
5 | import os
6 | import sqlite3
7 | import time
8 | import stdiomask
9 | from datetime import datetime
10 | from math import fabs
11 | from threading import Thread
12 | from binance.client import Client
13 | from binance.exceptions import BinanceAPIException, BinanceOrderException
14 | from binance.websockets import BinanceSocketManager
15 | from http.client import RemoteDisconnected
16 | from urllib3.exceptions import ProtocolError
17 | from requests.exceptions import ConnectionError
18 |
19 | file_path = os.path.abspath(os.path.dirname(__file__))
20 | os.chdir(file_path)
21 | data_path = os.path.dirname(os.path.abspath(__file__))
22 | data_path = os.path.join(data_path, 'data')
23 |
24 | logger = logging.getLogger(__name__)
25 |
26 |
27 | def check_symbol(symbol):
28 | safe_symbol = symbol
29 | if not symbol[0].isalpha():
30 | safe_symbol = symbol[1:]
31 | check_symbol(safe_symbol)
32 | return safe_symbol
33 |
34 |
35 | def setup():
36 | """Initial setup getting user input"""
37 | # os.system('cls' if os.name == 'nt' else 'clear')
38 | print('Welcome to perpSniper v0.2 *alpha version, not financial advice, use at your own risk!')
39 | print('Initial setup needed....')
40 | print('\n\n\n\n')
41 | settings = {'api_key': input('api_key: '), 'api_secret': stdiomask.getpass('api_secret: '),
42 | 'sl': float(input('stop loss percentage (price %, e.g. 0.5): ')) / 100,
43 | 'tp': float(input('take profit percentage (price %, e.g. 2.5): ')) / 100,
44 | 'db': float(input('trailing stop drawback (price %, e.g. 0.1): ')),
45 | 'qty': float(input('percentage stake (margin balance %, e.g. 5: ')) / 100}
46 | with open('settings.json', 'w') as f:
47 | json.dump(settings, f)
48 |
49 |
50 | def get_settings():
51 | """Check for and get settings"""
52 | try:
53 | keycheck = ['api_key', 'api_secret', 'sl', 'tp', 'db', 'qty']
54 | # file_path = os.path.dirname(__file__)
55 | # file = os.path.join(file_path, 'settings.json')
56 | with open('settings.json', 'r') as f:
57 | settings = json.load(f)
58 | for key in keycheck:
59 | if key not in settings.keys():
60 | raise KeyError
61 | return settings
62 | except (FileNotFoundError, KeyError) as e:
63 | print(e)
64 | setup()
65 | return get_settings()
66 |
67 |
68 | class Trade:
69 | def __init__(self,
70 | symbol: str,
71 | direction: bool,
72 | quantity: float,
73 | tp: float,
74 | sl: float,
75 | db: float,
76 | info: dict,
77 | trader,
78 | perpetual=False):
79 | """
80 | Create long or short trade.
81 | :param symbol: str denoting symbol pair to be traded, e.g. 'BTCUSDT'
82 | :param direction: bool indicating if trade direction is long (True) or short (False)
83 | :param quantity: float percentage of balance to be traded
84 | :param tp: float take profit activation price
85 | :param sl: float stop loss stop price
86 | :param db: float callback/drawback rate for trailing tp
87 | :param info: dict symbol exchange information such as 'pricePrecision'
88 | :param trader: Trader class object with Binance api client
89 | """
90 | self.date = datetime.now()
91 | self.symbol = symbol
92 | self.direction = direction
93 | # self.price = float(approx_price)
94 | self.quantity = float(quantity)
95 | self.trader = trader
96 | self.client = trader.client
97 | self.info = info
98 | self.tp = tp
99 | self.sl = sl
100 | self.db = float(db)
101 | self.price_decimals = f"{{:.{self.info['pricePrecision']}f}}"
102 | if not perpetual:
103 | self.trade()
104 | self.price = self.update_entry_price()
105 | self.stop_loss()
106 | self.take_profit()
107 |
108 | def update_entry_price(self):
109 | for position in self.trader.return_open_positions():
110 | if position['symbol'] == self.symbol:
111 | self.price = position['entry']
112 | return self.price
113 | time.sleep(0.1)
114 | self.update_entry_price()
115 |
116 | def trade(self):
117 | order_type = 'MARKET'
118 | side = 'BUY' if self.direction else 'SELL'
119 | try:
120 | self.client.futures_create_order(
121 | type=order_type,
122 | side=side,
123 | quantity=self.quantity,
124 | symbol=self.symbol,
125 | )
126 | except (BinanceAPIException, BinanceOrderException) as e:
127 | raise e
128 |
129 | def take_profit(self):
130 | order_type = 'TRAILING_STOP_MARKET'
131 | side = 'SELL' if self.direction else 'BUY'
132 | if self.direction:
133 | stop_price = float(self.price_decimals.format(self.price + (self.price * self.tp)))
134 | else:
135 | stop_price = float(self.price_decimals.format(self.price - (self.price * self.tp)))
136 | try:
137 | self.client.futures_create_order(
138 | type=order_type,
139 | side=side,
140 | quantity=self.quantity,
141 | reduceOnly=True,
142 | workingType='MARK_PRICE',
143 | symbol=self.symbol,
144 | activationPrice=stop_price,
145 | callbackRate=self.db
146 | )
147 | except (BinanceAPIException, BinanceOrderException) as e:
148 | raise e
149 |
150 | def stop_loss(self):
151 | order_type = 'STOP_MARKET'
152 | side = 'SELL' if self.direction else 'BUY'
153 | if self.direction:
154 | stop_price = float(self.price_decimals.format(self.price - (self.price * self.sl)))
155 | else:
156 | stop_price = float(self.price_decimals.format(self.price + (self.price * self.sl)))
157 | try:
158 | self.client.futures_create_order(
159 | type=order_type,
160 | side=side,
161 | quantity=self.quantity,
162 | reduceOnly=True,
163 | symbol=self.symbol,
164 | stopPrice=stop_price,
165 | workingType='MARK_PRICE',
166 | )
167 | except (BinanceAPIException, BinanceOrderException) as e:
168 | raise e
169 |
170 |
171 | class PerpetualTrade(Trade):
172 |
173 | trade_counter = 0
174 |
175 | def __init__(self, *args):
176 | super().__init__(*args, perpetual=True)
177 | if self.direction is True:
178 | self.long()
179 | elif self.direction is False:
180 | self.short()
181 | else:
182 | return
183 | self.update_entry_price()
184 | self.stop_loss()
185 |
186 | def long(self):
187 | if self.direction is True:
188 | self.trade()
189 | elif self.direction is False:
190 | self.reverse_trade()
191 | self.trade_counter += 1
192 |
193 | def short(self):
194 | if self.direction is False:
195 | self.trade()
196 | elif self.direction is True:
197 | self.reverse_trade()
198 | self.trade_counter += 1
199 |
200 | def flat(self):
201 | if self.trade_counter > 1:
202 | self.quantity /= 2
203 | self.trader.close_position(self.symbol)
204 | self.trade_counter = 0
205 | self.direction = None
206 |
207 | def reverse_trade(self):
208 | self.client.futures_cancel_all_open_orders(symbol=self.symbol)
209 | self.direction = False if self.direction else True
210 | if self.trade_counter == 1:
211 | self.quantity *= 2
212 | self.trade()
213 | try:
214 | self.update_entry_price()
215 | self.stop_loss()
216 | except (BinanceAPIException, BinanceOrderException) as e:
217 | raise e
218 |
219 |
220 | class Trader:
221 | def __init__(self):
222 | """
223 | Set up constants, and keep track of trades
224 | """
225 | self.threads = []
226 | self.LEVERAGE = 20
227 | self.config = []
228 | self.open_trades = {}
229 | self.mark_prices = {}
230 | self.open_positions_local = []
231 | self.settings = get_settings()
232 | self.client = Client(self.settings['api_key'], self.settings['api_secret'], requests_params={'timeout': 30})
233 | self.server_time = datetime.fromtimestamp(self.return_server_time()).strftime('%H:%M:%S')
234 | self.bsm_1 = BinanceSocketManager(self.client)
235 |
236 | def start_thread(self, func):
237 | t = Thread(target=func)
238 | t.setDaemon(True)
239 | t.start()
240 | self.threads.append(t)
241 |
242 | def stop_threads(self):
243 | for thread in self.threads:
244 | thread.join()
245 |
246 | def trade(self, symbol, direction):
247 | symbol = symbol.upper()
248 | quantity, approx_price, info = self.calculate_max_qty(symbol)
249 | try:
250 | t = Trade(symbol,
251 | direction,
252 | quantity,
253 | float(self.settings['tp']),
254 | float(self.settings['sl']),
255 | float(self.settings['db']),
256 | info,
257 | self)
258 | self.open_trades[t.date] = t
259 | except (BinanceAPIException, BinanceOrderException) as e:
260 | raise e
261 |
262 | @staticmethod
263 | def get_price(symbol):
264 | conn = sqlite3.connect('symbols.db')
265 | c = conn.cursor()
266 | symbol = check_symbol(symbol)
267 | try:
268 | q = f'SELECT * FROM {symbol}_15m WHERE date = (SELECT MAX(date) FROM {symbol}_15m)'
269 | price = c.execute(q).fetchone()[3]
270 | finally:
271 | conn.close()
272 | return price
273 |
274 | def get_account_info(self):
275 | ac_info = self.client.futures_account()
276 | maintenance = '{:.2f}'.format(float(ac_info['totalMaintMargin']))
277 | balance = '{:.2f}'.format(float(ac_info['totalWalletBalance']))
278 | total_pnl = '{:.2f}'.format(float(ac_info['totalUnrealizedProfit']))
279 | margin_balance = '{:.2f}'.format(float(ac_info['totalMarginBalance']))
280 | account_dict = {
281 | 'maintenance': maintenance,
282 | 'balance': balance,
283 | 'total_pnl': total_pnl,
284 | 'margin_balance': margin_balance
285 | }
286 | return account_dict
287 |
288 | def get_usdt_balance(self):
289 | return float(self.get_account_info()['balance'])
290 |
291 | def calculate_max_qty(self, symbol):
292 | price = float(self.get_price(symbol))
293 | usdt_bal = self.get_usdt_balance()
294 | affordable = usdt_bal / price
295 | qty = affordable * float(self.settings['qty']) * self.LEVERAGE
296 | info = [s for s in self.client.futures_exchange_info()['symbols'] if s['symbol'] == symbol][0]
297 | qty_precision = info['quantityPrecision']
298 | decimals = f'{{:.{qty_precision}f}}'
299 | qty = float(decimals.format(qty))
300 | return qty, price, info
301 |
302 | def return_open_positions(self):
303 | acc = self.client.futures_account()
304 | positions = acc['positions']
305 | position_list = []
306 | for position in positions:
307 | if float(position['positionAmt']) > 0:
308 | roe = (float(position['unrealizedProfit']) / (
309 | (float(position['positionAmt']) * float(position['entryPrice'])) / int(
310 | position['leverage']))) * 100
311 | direction = 'LONG'
312 | elif float(position['positionAmt']) < 0:
313 | roe = -(float(position['unrealizedProfit']) / (
314 | (float(position['positionAmt']) * float(position['entryPrice'])) / int(
315 | position['leverage']))) * 100
316 | direction = 'SHORT'
317 | else:
318 | continue
319 | position = {
320 | 'symbol': position['symbol'],
321 | 'qty': position['positionAmt'],
322 | 'entry': float(position['entryPrice']),
323 | 'pnl': float(position['unrealizedProfit']),
324 | 'roe': roe,
325 | 'direction': direction,
326 | }
327 | position_list.append(position)
328 | return position_list
329 |
330 | def positions_set(self):
331 | return set([position['symbol'] for position in self.return_open_positions()])
332 |
333 | def check_positions_cancel_open_orders(self):
334 | try:
335 | positions_symbols = set([position['symbol'] for position in self.return_open_positions()])
336 | orders = self.client.futures_get_open_orders()
337 | orders_symbols = set([order['symbol'] for order in orders])
338 | diff = orders_symbols.difference(positions_symbols)
339 | for s in diff:
340 | self.client.futures_cancel_all_open_orders(symbol=s)
341 | return orders_symbols
342 | except (RemoteDisconnected, ProtocolError, ConnectionError) as e:
343 | error = f'Binance connection error: {e}'
344 | logger.error(error)
345 | time.sleep(1)
346 | self.check_positions_cancel_open_orders()
347 |
348 | def close_position(self, symbol):
349 | for position in self.return_open_positions():
350 | if position['symbol'] == symbol:
351 | direction = True if position['direction'] == 'LONG' else False
352 | side = 'SELL' if direction else 'BUY'
353 | qty = fabs(float(position['qty']))
354 | self.client.futures_create_order(
355 | type='MARKET',
356 | reduceOnly=True,
357 | symbol=symbol,
358 | side=side,
359 | quantity=qty
360 | )
361 | self.check_positions_cancel_open_orders()
362 | break
363 |
364 | def close_all_positions(self):
365 | for position in self.return_open_positions():
366 | direction = True if position['direction'] == 'LONG' else False
367 | side = 'SELL' if direction else 'BUY'
368 | qty = fabs(float(position['qty']))
369 | self.client.futures_create_order(
370 | type='MARKET',
371 | reduceOnly=True,
372 | symbol=position['symbol'],
373 | side=side,
374 | quantity=qty
375 | )
376 |
377 | def return_server_time(self):
378 | time_dict = self.client.get_server_time()
379 | original_server_time = time_dict['serverTime']/1000
380 | return original_server_time
381 |
382 | def count_server_time(self, ost):
383 | while True:
384 | ost += 1
385 | time.sleep(1)
386 | self.server_time = datetime.fromtimestamp(ost).strftime('%H:%M:%S')
387 |
388 |
389 | if __name__ == '__main__':
390 | print(Trader.get_price('BTCUSDT'))
391 |
--------------------------------------------------------------------------------
/async_algo_trader.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | import os
4 | import pickle
5 | import sys
6 | import time
7 | from datetime import datetime, timedelta
8 |
9 | import requests
10 | from apscheduler.schedulers.background import BackgroundScheduler
11 | from trader import Trader
12 | from coin_data import CoinData
13 | from signals import Signals
14 |
15 | logger = logging.getLogger(__name__)
16 | logger.setLevel(logging.DEBUG)
17 | handler = logging.StreamHandler(sys.stdout)
18 | handler.setLevel(logging.INFO)
19 | formatter = logging.Formatter('%(asctime)s - [ %(levelname)s ] - %(message)s')
20 | handler.setFormatter(formatter)
21 | logger.addHandler(handler)
22 |
23 |
24 | # Telegram details
25 | BOT_TOKEN = os.getenv('tg_debug_bot_token')
26 | CHANNEL_ID = os.getenv('tg_debug_channel')
27 |
28 |
29 | class AlgoTrader:
30 |
31 | """Check each coin for signals and make trades in certain conditions.
32 | Conditions:
33 | - 15m RSI oversold/overbought and RSI divergence, and 1h ema_50/ema_200 trend.
34 | - 15m RSI oversold/overbought in last hour and macd crossing up/down."""
35 |
36 | data = CoinData()
37 | trader = Trader()
38 | scheduler = BackgroundScheduler()
39 |
40 | def __init__(self):
41 | self.signals_dict = {}
42 | self.trend_markers = {}
43 | self.rsi_markers = {}
44 | self.event_loop = None
45 | try:
46 | with open('pickle', 'rb') as f:
47 | data = pickle.load(f)
48 | if data['time'] >= datetime.now() - timedelta(minutes=15):
49 | self.recent_alerts = data['recent']
50 | self.ready_symbols = data['ready']
51 | print(f'ready symbols loaded: ' + ', '.join(self.ready_symbols))
52 | else:
53 | raise FileNotFoundError
54 | except (FileNotFoundError, EOFError):
55 | self.recent_alerts = []
56 | self.ready_symbols = {
57 | 'long': [],
58 | 'short': []
59 | }
60 | self.trader = Trader()
61 | self.trader.settings['sl'] = 0.005
62 | self.trader.settings['tp'] = 0.01
63 | self.trader.settings['qty'] = 0.01
64 | self.trader.settings['db'] = 0.1
65 | self.event_loop = asyncio.get_event_loop()
66 |
67 | @staticmethod
68 | async def create_signals_instance(symbol, tf):
69 | s = Signals(symbol, tf)
70 | return s
71 |
72 | async def get_signals(self):
73 | logger.debug('Getting signals')
74 | inadequate_symbols = []
75 | for symbol in self.data.symbols:
76 | try:
77 | m15 = asyncio.create_task(self.create_signals_instance(symbol, '15m'))
78 | h1 = asyncio.create_task(self.create_signals_instance(symbol, '1h'))
79 | h4 = asyncio.create_task(self.create_signals_instance(symbol, '4h'))
80 | self.signals_dict[symbol] = (await m15, await h1, await h4)
81 | except IndexError:
82 | inadequate_symbols.append(symbol)
83 | continue
84 | for symbol in inadequate_symbols:
85 | self.data.symbols.remove(symbol)
86 | return self.signals_dict
87 |
88 | async def record_trend(self):
89 | for symbol in self.data.symbols:
90 | signals_15m = self.signals_dict[symbol][0]
91 | signals_1h = self.signals_dict[symbol][1]
92 | signals_4h = self.signals_dict[symbol][2]
93 | h4 = True if signals_4h.df.ema_50.iloc[-1] > signals_4h.df.ema_200.iloc[-1] else False
94 | h1 = True if signals_1h.df.ema_50.iloc[-1] > signals_1h.df.ema_200.iloc[-1] else False
95 | m15 = True if signals_15m.df.ema_50.iloc[-1] > signals_15m.df.ema_200.iloc[-1] else False
96 | self.trend_markers[symbol] = (m15, h1, h4)
97 | return self.trend_markers
98 |
99 | async def purge_alerts(self):
100 | logger.debug('Purging alerts')
101 | old_alerts = []
102 | for alert in self.recent_alerts:
103 | split_alert = alert.split(' ')
104 | if datetime.strptime(' '.join(split_alert[3:5]), '%Y-%m-%d %H:%M:%S') < datetime.now() - timedelta(minutes=45):
105 | old_alerts.append(alert)
106 | for alert in old_alerts:
107 | self.recent_alerts.remove(alert)
108 |
109 | def check_rsi_div(self, symbol):
110 | if self.signals_dict[symbol][0].rsi_div_dict['confirmed bearish divergence']:
111 | return False
112 | elif self.signals_dict[symbol][0].rsi_div_dict['confirmed bullish divergence']:
113 | return True
114 | else:
115 | return None
116 |
117 | def check_macd(self, symbol):
118 | if self.signals_dict[symbol][0].macd_dict['MACD cross'] is False or self.signals_dict[symbol][0].macd_dict['MACD 0 cross'] is False:
119 | return False
120 | elif self.signals_dict[symbol][0].macd_dict['MACD cross'] is True or self.signals_dict[symbol][0].macd_dict['MACD 0 cross'] is True:
121 | return True
122 | else:
123 | return None
124 |
125 | def check_rsi_ob_os(self, symbol):
126 | if self.signals_dict[symbol][0].rsi_ob_os_dict['overbought']:
127 | return False
128 | elif self.signals_dict[symbol][0].rsi_ob_os_dict['oversold']:
129 | return True
130 | else:
131 | return None
132 |
133 | def check_4h_trend(self, symbol):
134 | if self.trend_markers[symbol][2] and self.trend_markers[symbol][1]:
135 | return True
136 | elif not self.trend_markers[symbol][2] and not self.trend_markers[symbol][1]:
137 | return False
138 | else:
139 | return None
140 |
141 | async def rsi_ob_os_marker(self, open_positions, recent_alerts):
142 | logger.debug('Checking RSI markers')
143 | for symbol in self.signals_dict.keys():
144 | if open_positions is not None and symbol not in open_positions and symbol not in recent_alerts:
145 | if self.check_4h_trend(symbol) is True:
146 | if self.check_rsi_ob_os(symbol) is True:
147 | if self.signals_dict[symbol][0].macd_dict['MACD up']:
148 | self.ready_symbols['long'].append(symbol)
149 | alert = f'LONG {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (RSI oversold signal)'
150 | self.handle_alert(alert)
151 | else:
152 | self.rsi_markers[symbol] = (True, datetime.now())
153 | elif self.check_4h_trend(symbol) is False:
154 | if self.check_rsi_ob_os(symbol) is False:
155 | if not self.signals_dict[symbol][0].macd_dict['MACD up']:
156 | self.ready_symbols['short'].append(symbol)
157 | alert = f'SHORT {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (RSI overbought signal)'
158 | self.handle_alert(alert)
159 | else:
160 | self.rsi_markers[symbol] = (False, datetime.now())
161 |
162 | async def purge_rsi_markers(self):
163 | logger.debug('Purging RSI markers')
164 | old_keys = []
165 | for key, value in self.rsi_markers.items():
166 | if value[1] < datetime.now() - timedelta(hours=1):
167 | old_keys.append(key)
168 | if old_keys:
169 | for key in old_keys:
170 | self.rsi_markers.pop(key, None)
171 |
172 | async def rsi_div_trade(self, open_positions, recent_alerts):
173 | logger.debug('Checking RSI div')
174 | for symbol in self.signals_dict.keys():
175 | if open_positions is not None and symbol not in open_positions and symbol not in recent_alerts:
176 | if self.check_4h_trend(symbol) is True:
177 | if self.check_rsi_div(symbol) is True:
178 | self.ready_symbols['long'].append(symbol)
179 | # self.trader.trade(symbol, True)
180 | alert = f'LONG {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (RSI div signal)'
181 | self.handle_alert(alert)
182 | elif self.check_4h_trend(symbol) is False:
183 | if self.check_rsi_div(symbol) is False:
184 | self.ready_symbols['short'].append(symbol)
185 | # self.trader.trade(symbol, False)
186 | alert = f'SHORT {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (RSI div signal)'
187 | self.handle_alert(alert)
188 |
189 | async def rsi_macd_trade(self, open_positions, recent_alerts):
190 | logger.debug('Checking MACD')
191 | for symbol in self.signals_dict.keys():
192 | if symbol in self.rsi_markers.keys():
193 | if open_positions is not None and symbol not in open_positions and symbol not in recent_alerts:
194 | if self.rsi_markers[symbol][0]:
195 | if self.check_4h_trend(symbol) is True:
196 | if self.check_macd(symbol) is True:
197 | # self.trader.trade(symbol, True)
198 | self.ready_symbols['long'].append(symbol)
199 | alert = f'LONG {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (MACD signal)'
200 | self.handle_alert(alert)
201 | else:
202 | if self.check_4h_trend(symbol) is False:
203 | if self.check_macd(symbol) is False:
204 | # self.trader.trade(symbol, False)
205 | self.ready_symbols['short'].append(symbol)
206 | alert = f'SHORT {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (MACD signal)'
207 | self.handle_alert(alert)
208 |
209 | async def ha_long(self, open_positions, recent_alerts):
210 | trades = []
211 | for symbol in self.ready_symbols['long']:
212 | if open_positions is not None and symbol not in open_positions and symbol not in recent_alerts:
213 | if Signals.get_heiken_ashi_trend(Signals.get_heiken_ashi(CoinData.get_dataframe(symbol, '15m'))) is True:
214 | self.trader.trade(symbol, True)
215 | alert = f'LONGED {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} HEIKEN ASHI FINAL SIGNAL'
216 | trades.append(symbol)
217 | self.handle_alert(alert)
218 | for s in trades:
219 | self.ready_symbols['long'].remove(s)
220 |
221 | async def ha_short(self, open_positions, recent_alerts):
222 | trades = []
223 | for symbol in self.ready_symbols['short']:
224 | if open_positions is not None and symbol not in open_positions and symbol not in recent_alerts:
225 | if Signals.get_heiken_ashi_trend(Signals.get_heiken_ashi(CoinData.get_dataframe(symbol, '15m'))) is False:
226 | self.trader.trade(symbol, False)
227 | alert = f'SHORTED {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} HEIKEN ASHI FINAL SIGNAL'
228 | trades.append(symbol)
229 | self.handle_alert(alert)
230 | for s in trades:
231 | self.ready_symbols['short'].remove(s)
232 |
233 | async def check_heiken_ashi(self, open_positions, recent_alerts_symbols):
234 | logger.debug('Checking final condition (Heiken Ashi)')
235 | long_task = asyncio.create_task(self.ha_long(open_positions, recent_alerts_symbols))
236 | short_task = asyncio.create_task(self.ha_short(open_positions, recent_alerts_symbols))
237 | await long_task
238 | await short_task
239 |
240 | def handle_alert(self, alert):
241 | self.recent_alerts.append(alert)
242 | logger.info(alert)
243 | self.send_message(alert)
244 |
245 | def send_message(self, message):
246 | message = 'TRADING BOT ALERT: ' + message
247 | requests.get(f'https://api.telegram.org/bot{BOT_TOKEN}/sendMessage?chat_id={CHANNEL_ID}&text={message}')
248 |
249 | async def get_recent_alerts(self):
250 | return [alert.split(' ')[1] for alert in self.recent_alerts]
251 |
252 | async def get_open_positions(self):
253 | return self.trader.check_positions_cancel_open_orders()
254 |
255 | async def check_conditions(self):
256 | start_time = datetime.now()
257 | logger.debug('Starting check')
258 | # Get new signals data
259 | await self.get_signals()
260 | task0 = asyncio.create_task(self.record_trend())
261 | task1 = asyncio.create_task(self.get_recent_alerts())
262 | task2 = asyncio.create_task(self.get_open_positions())
263 | # co_1 = [self.record_trend(),
264 | # self.get_recent_alerts(),
265 | # self.get_open_positions()]
266 | await task0
267 | recent_alerts_symbols = await task1
268 | open_positions = await task2
269 |
270 | log_statement = 'took: {}'.format(datetime.now() - start_time)
271 | logger.debug(log_statement)
272 |
273 | # Check trade conditions
274 | task_1 = asyncio.create_task(self.rsi_div_trade(open_positions,
275 | recent_alerts_symbols))
276 | task_3 = asyncio.create_task(self.rsi_ob_os_marker(open_positions,
277 | recent_alerts_symbols))
278 | task_2 = asyncio.create_task(self.rsi_macd_trade(open_positions,
279 | recent_alerts_symbols))
280 | await task_1
281 | await task_3
282 | await task_2
283 |
284 | heiken_ashi_check = asyncio.create_task(self.check_heiken_ashi(open_positions,
285 | recent_alerts_symbols))
286 | task_1 = asyncio.create_task(self.purge_alerts())
287 | task_2 = asyncio.create_task(self.purge_rsi_markers())
288 | await heiken_ashi_check
289 | await task_1
290 | await task_2
291 |
292 | if self.recent_alerts:
293 | recent = ', '.join(self.recent_alerts)
294 | logger.debug(recent)
295 | total_time = datetime.now() - start_time
296 | log_statement = 'total_time: {}'.format(total_time)
297 | logger.debug(log_statement)
298 | if self.recent_alerts or self.ready_symbols:
299 | self.debug_statements()
300 |
301 | def save_data(self):
302 | self.data.save_latest_data()
303 |
304 | def schedule_tasks(self):
305 | self.scheduler.add_job(self.save_data, trigger='cron', minute='*/1', second='2')
306 | self.scheduler.start()
307 |
308 | def stop_tasks(self):
309 | self.scheduler.remove_all_jobs()
310 | self.scheduler.shutdown()
311 |
312 | def loop(self):
313 | try:
314 | asyncio.run(self.get_signals())
315 | self.schedule_tasks()
316 | while datetime.now().second != 3:
317 | time.sleep(1)
318 | log_statement = f'Starting mainloop at {datetime.now().strftime("%H:%M:%S")}'
319 | logger.info(log_statement)
320 | while True:
321 | asyncio.run(self.check_conditions())
322 | while datetime.now().second != 3:
323 | time.sleep(1)
324 | except KeyboardInterrupt as e:
325 | self.stop_tasks()
326 | data = {
327 | 'recent': self.recent_alerts,
328 | 'ready': self.ready_symbols,
329 | 'time': datetime.now()
330 | }
331 | with open('pickle', 'wb') as f:
332 | pickle.dump(data, f)
333 | sys.exit()
334 |
335 | def debug_statements(self):
336 | log = 'Recent alerts: ' + ', '.join(self.recent_alerts)
337 | logger.debug(log)
338 | log = 'Ready symbols long: ' + ', '.join(self.ready_symbols['long'])
339 | logger.debug(log)
340 | log = 'Ready symbols short: ' + ', '.join(self.ready_symbols['short'])
341 | logger.debug(log)
342 |
343 |
344 | if __name__ == '__main__':
345 | at = AlgoTrader()
346 | at.loop()
347 |
--------------------------------------------------------------------------------
/static/js/index.js:
--------------------------------------------------------------------------------
1 | function loadUrl(newLocation){
2 | window.location = newLocation;
3 | return false;
4 | }
5 |
6 | function getRecentAlerts(){
7 | var table = document.getElementById('recent_alerts_table')
8 | var thead = document.getElementById('recent_alerts')
9 | // var hotCoins = document.getElementById('hot_coins')
10 | fetch(`${window.origin}/api/signals`)
11 | .then(function(response){
12 | if (response.status !== 200) {
13 | displayMessage(`Bad response from api: ${response.status}`)
14 | return ;
15 | }
16 | response.json().then(function(data){
17 | var alertRows = document.getElementsByClassName('alert_row')
18 | thead.innerHTML = '';
19 | data.signals.sort(function(a,b){
20 | var c = new Date(a.date);
21 | var d = new Date(b.date);
22 | return c-d;
23 | });
24 | data.signals.reverse();
25 | for (i = 0; i < data.signals.length; i++) {
26 | var row = thead.insertRow(0);
27 | row.classList.add('alert_row')
28 | var words = data.signals[i].alert.split(' ');
29 | var bullish = ['up', 'bullish', 'oversold']
30 | var bearish = ['down', 'bearish', 'overbought']
31 | for (j = 0; j < words.length; j++) {
32 | for (k = 0; k < bullish.length; k++) {
33 | if (words[j] == bullish[k]) {
34 | row.classList.add('text-green-500')
35 | }
36 | }
37 | for (k = 0; k < bearish.length; k++) {
38 | if (words[j] == bearish[k]) {
39 | row.classList.add('text-red-500')
40 | }
41 | }
42 | }
43 | var cell1 = row.insertCell(0);
44 | var cell2 = row.insertCell(1);
45 | var cell3 = row.insertCell(2);
46 | cell1.innerHTML = data.signals[i].time;
47 | cell2.innerHTML = data.signals[i].symbol;
48 | cell2.classList.add('font-bold')
49 | cell2.addEventListener('click', changeChart, false);
50 | cell3.innerHTML = data.signals[i].alert;
51 | }
52 | // hot_coins.innerHTML = ''
53 | // for (i = 0; i < data.hot_coins.length; i++) {
54 | // var entry = document.createElement('li')
55 | // entry.innerHTML = `${data.hot_coins[i][0]} (${data.hot_coins[i][1]} alerts)`
56 | // hotCoins.appendChild(entry)
57 | // }
58 | return ;
59 | })
60 | })
61 | }
62 |
63 |
64 | function openLong() {
65 | var coin = document.getElementById('coinInput')
66 | fetch(`${window.origin}/api/long`, {
67 | method: 'post',
68 | headers: {
69 | 'Content-Type': 'application/json'
70 | // 'Content-Type': 'application/x-www-form-urlencoded',
71 | },
72 | body: JSON.stringify({coin: coinInput.value}),
73 | })
74 | .then(function(response){
75 | if (response.status !== 200) {
76 | displayMessage(`Bad response from api: ${response.status}`)
77 | return ;
78 | }
79 | else {displayMessage(`Opened LONG on: ${coin.value}`)}
80 | })
81 | }
82 |
83 |
84 | function openShort() {
85 | var coin = document.getElementById('coinInput')
86 | fetch(`${window.origin}/api/short`, {
87 | method: 'post',
88 | headers: {
89 | 'Content-Type': 'application/json'
90 | // 'Content-Type': 'application/x-www-form-urlencoded',
91 | },
92 | body: JSON.stringify({coin: coinInput.value}),
93 | })
94 | .then(function(response){
95 | if (response.status !== 200) {
96 | displayMessage(`Bad response from api: ${response.status}`)
97 | return ;
98 | }
99 | else {displayMessage(`Opened SHORT on: ${coin.value}`)}
100 | })
101 | }
102 |
103 | function openQuickShort(coin) {
104 | fetch(`${window.origin}/api/short`, {
105 | method: 'post',
106 | headers: {
107 | 'Content-Type': 'application/json'
108 | // 'Content-Type': 'application/x-www-form-urlencoded',
109 | },
110 | body: JSON.stringify({coin: coin}),
111 | })
112 | .then(function(response){
113 | if (response.status !== 200) {
114 | displayMessage(`Bad response from api: ${response.status}`)
115 | return ;
116 | }
117 | else {
118 | response.json().then(function(data) {
119 | if (data.message) {
120 | displayMessage(data.message)
121 | }
122 | })
123 | }
124 | })
125 | }
126 |
127 | function testMessage() {
128 | displayMessage('Hello World');
129 | }
130 |
131 | function openQuickLong(coin) {
132 | fetch(`${window.origin}/api/long`, {
133 | method: 'post',
134 | headers: {
135 | 'Content-Type': 'application/json'
136 | // 'Content-Type': 'application/x-www-form-urlencoded',
137 | },
138 | body: JSON.stringify({coin: coin}),
139 | })
140 | .then(function(response){
141 | if (response.status !== 200) {
142 | displayMessage(`Bad response from api: ${response.status}`)
143 | return ;
144 | }
145 | else {
146 | response.json().then(function(data) {
147 | if (data.message) {
148 | displayMessage(data.message)
149 | }
150 | })
151 | }
152 | })
153 | }
154 |
155 | function shutDown() {
156 | fetch(`${window.origin}/shutdown`, {
157 | method: 'post'})
158 | }
159 |
160 | function getPositions() {
161 | fetch(`${window.origin}/api/positions`)
162 | .then(function(response){
163 | if (response.status !== 200) {
164 | displayMessage(`Bad response from api: ${response.status}`)
165 | return ;
166 | }
167 | response.json().then(function(data){
168 | var tBody = document.getElementById('tBody')
169 | tBody.innerHTML = ''
170 | for (i = 0; i < data.positions.length; i++) {
171 | var row = tBody.insertRow(0);
172 | // row.classList.add('')
173 | var cell1 = row.insertCell(0);
174 | var cell2 = row.insertCell(1);
175 | var cell3 = row.insertCell(2);
176 | var cell4 = row.insertCell(3);
177 | var cell5 = row.insertCell(4);
178 | cell1.innerHTML = data.positions[i].symbol;
179 | cell1.addEventListener('click', changeChart, false);
180 | cell2.innerHTML = data.positions[i].direction;
181 | cell3.innerHTML = data.positions[i].qty;
182 | cell4.innerHTML = '$' + data.positions[i].pnl.toFixed(2);
183 | cell5.innerHTML = data.positions[i].roe.toFixed(2) +'%';
184 | }
185 | })
186 | })
187 | }
188 |
189 | function getBalance() {
190 | fetch(`${window.origin}/api/account`)
191 | .then(function(response){
192 | if (response.status !== 200) {
193 | displayMessage(`Bad response from api: ${response.status}`)
194 | return ;
195 | }
196 | response.json().then(function(data){
197 | var tBody = document.getElementById('accountTBody')
198 | var balance = document.getElementById('balance')
199 | var pnl = document.getElementById('pnl')
200 | balance.innerHTML = 'Balance: $' + parseFloat(data.balance).toFixed(2);
201 | pnl.innerHTML = 'PNL: $' + parseFloat(data.total_pnl).toFixed(2);
202 | // tBody.innerHTML = ''
203 | // var row = tBody.insertRow(0);
204 | // row.classList.add('text-left')
205 | // var cell1 = row.insertCell(0);
206 | // var cell2 = row.insertCell(1);
207 | // var cell3 = row.insertCell(2);
208 | // cell1.innerHTML = '$' + parseFloat(data.balance).toFixed(2);
209 | // cell2.innerHTML = '$' + parseFloat(data.total_pnl).toFixed(2);
210 | // cell3.innerHTML = '$' + parseFloat(data.margin_balance).toFixed(2);
211 | })
212 | })
213 | }
214 |
215 | function closeOldOrders(){
216 | fetch(`${window.origin}/api/positions`)
217 | .then(function(response){
218 | if (response.status !== 200) {
219 | displayMessage(`Bad response from api: ${response.status}`)
220 | return ;
221 | }
222 | else {
223 | displayMessage('Orders cleared')
224 | }
225 | })
226 | }
227 |
228 | var xDown = null;
229 | var yDown = null;
230 |
231 | function getTouches(evt) {
232 | return evt.touches || // browser API
233 | evt.originalEvent.touches; // jQuery
234 | }
235 |
236 | function handleTouchStart(evt) {
237 | const firstTouch = getTouches(evt)[0];
238 | xDown = firstTouch.clientX;
239 | yDown = firstTouch.clientY;
240 | };
241 |
242 | function handleTouchMove(evt) {
243 | if ( ! xDown || ! yDown ) {
244 | return;
245 | }
246 |
247 | var xUp = evt.touches[0].clientX;
248 | var yUp = evt.touches[0].clientY;
249 |
250 | var xDiff = xDown - xUp;
251 | var yDiff = yDown - yUp;
252 |
253 | var coinName = this.id
254 | var closeId = `${coinName}Close`
255 |
256 |
257 | if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {
258 | if ( xDiff > 0 ) {
259 | $(this).css({
260 | 'animation': 'slideSwipeDrawerOpen .2s linear',
261 | '-webkit-transform' : 'translateX(' + -6 + 'rem)',
262 | '-moz-transform' : 'translateX(' + -6 + 'rem)',
263 | '-ms-transform' : 'translateX(' + -6 + 'rem)',
264 | '-o-transform' : 'translateX(' + -6 + 'rem)',
265 | 'transform' : 'translateX(' + -6 + 'rem)'
266 | });
267 | document.getElementById(closeId).style.display = 'flex'
268 |
269 | } else {
270 | $(this).css({
271 | 'animation': 'slideSwipeDrawerClose .2s linear',
272 | '-webkit-transform' : 'translateX(' + 0 + 'rem)',
273 | '-moz-transform' : 'translateX(' + 0 + 'rem)',
274 | '-ms-transform' : 'translateX(' + 0 + 'rem)',
275 | '-o-transform' : 'translateX(' + 0 + 'rem)',
276 | 'transform' : 'translateX(' + 0 + 'rem)'
277 | });
278 | document.getElementById(closeId).style.display = 'none'
279 | }
280 | }
281 | /* reset values */
282 | xDown = null;
283 | yDown = null;
284 | };
285 |
286 | function handleRightClick(evt) {
287 | var coinName = this.id
288 | var closeId = `${coinName}Close`
289 | evt.preventDefault()
290 | if (this.getAttribute('data-open')) {
291 | document.getElementById(closeId).style.display = 'none'
292 | $(this).css({
293 | 'animation': 'slideSwipeDrawerClose .2s linear',
294 | '-webkit-transform' : 'translateX(' + 0 + 'rem)',
295 | '-moz-transform' : 'translateX(' + 0 + 'rem)',
296 | '-ms-transform' : 'translateX(' + 0 + 'rem)',
297 | '-o-transform' : 'translateX(' + 0 + 'rem)',
298 | 'transform' : 'translateX(' + 0 + 'rem)'
299 | });
300 | this.removeAttribute('data-open')
301 | }
302 | else {
303 | document.getElementById(closeId).style.display = 'flex'
304 | $(this).css({
305 | 'animation': 'slideSwipeDrawerOpen .2s linear',
306 | '-webkit-transform' : 'translateX(' + -6 + 'rem)',
307 | '-moz-transform' : 'translateX(' + -6 + 'rem)',
308 | '-ms-transform' : 'translateX(' + -6 + 'rem)',
309 | '-o-transform' : 'translateX(' + -6 + 'rem)',
310 | 'transform' : 'translateX(' + -6 + 'rem)'
311 | });
312 | this.setAttribute('data-open', true)
313 | }
314 | }
315 |
316 | function removeCoin(coin) {
317 | coin.parentNode.parentNode.parentNode.removeChild(coin.parentNode.parentNode);
318 | }
319 |
320 | function closeOpenPositions(coinCloseIcon) {
321 | var coin = coinCloseIcon.parentNode.parentNode.parentNode.id
322 | fetch(`${window.origin}/api/close_all`, {
323 | method: 'post',
324 | headers: {
325 | 'Content-Type': 'application/json'
326 | // 'Content-Type': 'application/x-www-form-urlencoded',
327 | },
328 | body: JSON.stringify({coin: coin}),
329 | })
330 | .then(function(response){
331 | if (response.status !== 200) {
332 | displayMessage(`Bad response from api: ${response.status}`)
333 | return ;
334 | }
335 | else {displayMessage(`Closed position: ${coin}`);}
336 | })
337 | }
338 |
339 | function closeOpenPosition(coinCloseIcon) {
340 | var coin = coinCloseIcon.parentNode.parentNode.parentNode.id
341 | fetch(`${window.origin}/api/close_position`, {
342 | method: 'post',
343 | headers: {
344 | 'Content-Type': 'application/json'
345 | // 'Content-Type': 'application/x-www-form-urlencoded',
346 | },
347 | body: JSON.stringify({coin: coin}),
348 | })
349 | .then(function(response){
350 | if (response.status !== 200) {
351 | displayMessage(`Bad response from api: ${response.status}`)
352 | return ;
353 | }
354 | else {displayMessage(`Closed position: ${coin}`);}
355 | })
356 | }
357 |
358 | function apiPostRequest(endpoint, data, responseSuccessFunc) {
359 | fetch(`${window.origin}/${endpoint}`, {
360 | method: 'post',
361 | headers: {
362 | 'Content-Type': 'application/json'
363 | // 'Content-Type': 'application/x-www-form-urlencoded',
364 | },
365 | body: JSON.stringify(data),
366 | })
367 | .then(function(response){
368 | if (response.status !== 200) {
369 | displayMessage(`Bad response from api: ${response.status}`)
370 | return ;
371 | }
372 | else {responseSuccessFunc(response)}
373 | })
374 | }
375 |
376 | function closeAllPositions() {
377 | var data = {}
378 | apiPostRequest('/api/close_all_positions', data, function(response) {displayMessage('Positions closed')})
379 | }
380 |
381 | function addCoin() {
382 | var quickTradeDiv = document.getElementById('quickTradeDiv')
383 | var coin = document.getElementById('coinInput')
384 | coin_value = coin.value
385 | coin.value = ''
386 | coin = coin_value.toUpperCase()
387 | coin += 'USDT'
388 | // var quickTradeComponent = document.createElement('div')
389 | var innerDiv = document.createElement('div')
390 | innerDiv.innerHTML = `
`;
391 | innerDiv.id = coin
392 | innerDiv.style.opacity = 1
393 | innerDiv.style.animation = 'fadeIn 1s linear'
394 | innerDiv.addEventListener('touchstart', handleTouchStart, false);
395 | innerDiv.addEventListener('touchmove', handleTouchMove, false);
396 | innerDiv.addEventListener('contextmenu', handleRightClick, false);
397 | quickTradeDiv.appendChild(innerDiv)
398 | document.getElementById(`${coin}QuickTradeCoinText`).addEventListener('click', changeChart, false);
399 | // quickTradeDiv.appendChild(quickTradeComponent)
400 | }
401 |
402 | function hideMessages() {
403 | document.getElementById('messageBoxDiv').innerHTML = ''
404 | }
405 |
406 | function displayMessage(message) {
407 | var messageDiv = document.getElementById('messageBoxDiv')
408 | var component = `
${message}
`
409 | messageDiv.innerHTML = component;
410 | setTimeout(hideMessages, 3000);
411 |
412 | }
413 |
414 | function updateChart() {
415 | // console.log('updating chart');
416 | var coinText = document.getElementById('coinText')
417 | var intervalText = document.getElementById('intervalText').innerHTML
418 | var coin = coinText.innerHTML
419 | // console.log(coin)
420 | fetch(`${window.origin}/plot`, {
421 | method: 'post',
422 | headers: {
423 | 'Content-Type': 'application/json'
424 | // 'Content-Type': 'application/x-www-form-urlencoded',
425 | },
426 | body: JSON.stringify({
427 | symbol: coin,
428 | interval: intervalText
429 | }),
430 | })
431 | .then(function(response){
432 | if (response.status !== 200) {
433 | displayMessage(`Bad response from chart api: ${response.status}`)
434 | return ;
435 | }
436 | response.json().then(function(data){
437 | var chartSpace = document.getElementById('chartSpace')
438 | var imgTag = new Image();
439 | imgTag.onload = function() {
440 | chartSpace.setAttribute('src', data.base64)
441 | }
442 | imgTag.src = data.base64;
443 | })
444 | })
445 | }
446 |
447 | function changeChart() {
448 | var symbol = this.innerHTML
449 | var coinText = document.getElementById('coinText')
450 | coinText.innerHTML = symbol
451 | updateChart()
452 | // console.log(symbol)
453 | // apiPostRequest('/plot', data, function(response) {updateChart()})
454 | }
455 |
456 | var chartInterval = 10000;
457 | var intervalId;
458 |
459 | function getInterval() {
460 | return chartInterval
461 | }
462 |
463 | function startInterval(_interval) {
464 | intervalId = setInterval(function() {
465 | updateChart()
466 | }, _interval);
467 | }
468 |
469 | function selectInterval(btn) {
470 | var intervalText = document.getElementById('intervalText')
471 | var interval = btn.innerHTML
472 | intervalText.innerHTML = interval
473 | switch(interval) {
474 | case '1m':
475 | chartInterval = 2000;
476 | break;
477 | case '15m':
478 | chartInterval = 5000;
479 | break;
480 | case '1h':
481 | chartInterval = 10000;
482 | break;
483 | case '4h':
484 | chartInterval = 10000;
485 | break;
486 | }
487 | // console.log(interval)
488 | // console.log(chartInterval)
489 | clearInterval(intervalId);
490 | startInterval(chartInterval);
491 | updateChart();
492 | }
493 |
494 | function startTime(clock) {
495 | var today = new Date();
496 | var h = today.getHours();
497 | var m = today.getMinutes();
498 | var s = today.getSeconds();
499 | m = checkTime(m);
500 | s = checkTime(s);
501 | clock.innerHTML =
502 | h + ":" + m + ":" + s;
503 | var t = setTimeout(function(){startTime(clock)}, 500);
504 | }
505 | function checkTime(i) {
506 | if (i < 10) {i = "0" + i}; // add zero in front of numbers < 10
507 | return i;
508 | }
--------------------------------------------------------------------------------
/static/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "perp_sniper",
3 | "version": "0.1.1",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@fullhuman/postcss-purgecss": {
8 | "version": "3.1.3",
9 | "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz",
10 | "integrity": "sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA==",
11 | "requires": {
12 | "purgecss": "^3.1.3"
13 | }
14 | },
15 | "acorn": {
16 | "version": "7.4.1",
17 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
18 | "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
19 | },
20 | "acorn-node": {
21 | "version": "1.8.2",
22 | "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
23 | "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==",
24 | "requires": {
25 | "acorn": "^7.0.0",
26 | "acorn-walk": "^7.0.0",
27 | "xtend": "^4.0.2"
28 | }
29 | },
30 | "acorn-walk": {
31 | "version": "7.2.0",
32 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
33 | "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA=="
34 | },
35 | "ansi-styles": {
36 | "version": "4.3.0",
37 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
38 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
39 | "requires": {
40 | "color-convert": "^2.0.1"
41 | }
42 | },
43 | "at-least-node": {
44 | "version": "1.0.0",
45 | "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
46 | "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="
47 | },
48 | "autoprefixer": {
49 | "version": "10.2.4",
50 | "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.4.tgz",
51 | "integrity": "sha512-DCCdUQiMD+P/as8m3XkeTUkUKuuRqLGcwD0nll7wevhqoJfMRpJlkFd1+MQh1pvupjiQuip42lc/VFvfUTMSKw==",
52 | "requires": {
53 | "browserslist": "^4.16.1",
54 | "caniuse-lite": "^1.0.30001181",
55 | "colorette": "^1.2.1",
56 | "fraction.js": "^4.0.13",
57 | "normalize-range": "^0.1.2",
58 | "postcss-value-parser": "^4.1.0"
59 | }
60 | },
61 | "balanced-match": {
62 | "version": "1.0.0",
63 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
64 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
65 | },
66 | "brace-expansion": {
67 | "version": "1.1.11",
68 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
69 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
70 | "requires": {
71 | "balanced-match": "^1.0.0",
72 | "concat-map": "0.0.1"
73 | }
74 | },
75 | "browserslist": {
76 | "version": "4.16.3",
77 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz",
78 | "integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==",
79 | "requires": {
80 | "caniuse-lite": "^1.0.30001181",
81 | "colorette": "^1.2.1",
82 | "electron-to-chromium": "^1.3.649",
83 | "escalade": "^3.1.1",
84 | "node-releases": "^1.1.70"
85 | }
86 | },
87 | "bytes": {
88 | "version": "3.1.0",
89 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
90 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
91 | },
92 | "camelcase-css": {
93 | "version": "2.0.1",
94 | "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
95 | "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
96 | },
97 | "caniuse-lite": {
98 | "version": "1.0.30001187",
99 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001187.tgz",
100 | "integrity": "sha512-w7/EP1JRZ9552CyrThUnay2RkZ1DXxKe/Q2swTC4+LElLh9RRYrL1Z+27LlakB8kzY0fSmHw9mc7XYDUKAKWMA=="
101 | },
102 | "chalk": {
103 | "version": "4.1.0",
104 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
105 | "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
106 | "requires": {
107 | "ansi-styles": "^4.1.0",
108 | "supports-color": "^7.1.0"
109 | }
110 | },
111 | "color": {
112 | "version": "3.1.3",
113 | "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz",
114 | "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==",
115 | "requires": {
116 | "color-convert": "^1.9.1",
117 | "color-string": "^1.5.4"
118 | },
119 | "dependencies": {
120 | "color-convert": {
121 | "version": "1.9.3",
122 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
123 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
124 | "requires": {
125 | "color-name": "1.1.3"
126 | }
127 | },
128 | "color-name": {
129 | "version": "1.1.3",
130 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
131 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
132 | }
133 | }
134 | },
135 | "color-convert": {
136 | "version": "2.0.1",
137 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
138 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
139 | "requires": {
140 | "color-name": "~1.1.4"
141 | }
142 | },
143 | "color-name": {
144 | "version": "1.1.4",
145 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
146 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
147 | },
148 | "color-string": {
149 | "version": "1.5.4",
150 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
151 | "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
152 | "requires": {
153 | "color-name": "^1.0.0",
154 | "simple-swizzle": "^0.2.2"
155 | }
156 | },
157 | "colorette": {
158 | "version": "1.2.1",
159 | "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
160 | "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw=="
161 | },
162 | "commander": {
163 | "version": "6.2.1",
164 | "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
165 | "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="
166 | },
167 | "concat-map": {
168 | "version": "0.0.1",
169 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
170 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
171 | },
172 | "css-unit-converter": {
173 | "version": "1.1.2",
174 | "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
175 | "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA=="
176 | },
177 | "cssesc": {
178 | "version": "3.0.0",
179 | "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
180 | "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
181 | },
182 | "defined": {
183 | "version": "1.0.0",
184 | "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
185 | "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM="
186 | },
187 | "detective": {
188 | "version": "5.2.0",
189 | "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
190 | "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==",
191 | "requires": {
192 | "acorn-node": "^1.6.1",
193 | "defined": "^1.0.0",
194 | "minimist": "^1.1.1"
195 | }
196 | },
197 | "didyoumean": {
198 | "version": "1.2.1",
199 | "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz",
200 | "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8="
201 | },
202 | "electron-to-chromium": {
203 | "version": "1.3.666",
204 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.666.tgz",
205 | "integrity": "sha512-/mP4HFQ0fKIX4sXltG6kfcoGrfNDZwCIyWbH2SIcVaa9u7Rm0HKjambiHNg5OEruicTl9s1EwbERLwxZwk19aw=="
206 | },
207 | "escalade": {
208 | "version": "3.1.1",
209 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
210 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
211 | },
212 | "escape-string-regexp": {
213 | "version": "1.0.5",
214 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
215 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
216 | },
217 | "fraction.js": {
218 | "version": "4.0.13",
219 | "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.13.tgz",
220 | "integrity": "sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA=="
221 | },
222 | "fs-extra": {
223 | "version": "9.1.0",
224 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
225 | "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
226 | "requires": {
227 | "at-least-node": "^1.0.0",
228 | "graceful-fs": "^4.2.0",
229 | "jsonfile": "^6.0.1",
230 | "universalify": "^2.0.0"
231 | }
232 | },
233 | "fs.realpath": {
234 | "version": "1.0.0",
235 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
236 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
237 | },
238 | "function-bind": {
239 | "version": "1.1.1",
240 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
241 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
242 | },
243 | "glob": {
244 | "version": "7.1.6",
245 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
246 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
247 | "requires": {
248 | "fs.realpath": "^1.0.0",
249 | "inflight": "^1.0.4",
250 | "inherits": "2",
251 | "minimatch": "^3.0.4",
252 | "once": "^1.3.0",
253 | "path-is-absolute": "^1.0.0"
254 | }
255 | },
256 | "graceful-fs": {
257 | "version": "4.2.6",
258 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
259 | "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
260 | },
261 | "has": {
262 | "version": "1.0.3",
263 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
264 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
265 | "requires": {
266 | "function-bind": "^1.1.1"
267 | }
268 | },
269 | "has-flag": {
270 | "version": "4.0.0",
271 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
272 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
273 | },
274 | "html-tags": {
275 | "version": "3.1.0",
276 | "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
277 | "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg=="
278 | },
279 | "indexes-of": {
280 | "version": "1.0.1",
281 | "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
282 | "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
283 | },
284 | "inflight": {
285 | "version": "1.0.6",
286 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
287 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
288 | "requires": {
289 | "once": "^1.3.0",
290 | "wrappy": "1"
291 | }
292 | },
293 | "inherits": {
294 | "version": "2.0.4",
295 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
296 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
297 | },
298 | "is-arrayish": {
299 | "version": "0.3.2",
300 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
301 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
302 | },
303 | "is-core-module": {
304 | "version": "2.2.0",
305 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
306 | "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
307 | "requires": {
308 | "has": "^1.0.3"
309 | }
310 | },
311 | "jsonfile": {
312 | "version": "6.1.0",
313 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
314 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
315 | "requires": {
316 | "graceful-fs": "^4.1.6",
317 | "universalify": "^2.0.0"
318 | }
319 | },
320 | "lodash": {
321 | "version": "4.17.20",
322 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
323 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
324 | },
325 | "lodash.toarray": {
326 | "version": "4.4.0",
327 | "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
328 | "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
329 | },
330 | "minimatch": {
331 | "version": "3.0.4",
332 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
333 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
334 | "requires": {
335 | "brace-expansion": "^1.1.7"
336 | }
337 | },
338 | "minimist": {
339 | "version": "1.2.5",
340 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
341 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
342 | },
343 | "modern-normalize": {
344 | "version": "1.0.0",
345 | "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.0.0.tgz",
346 | "integrity": "sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw=="
347 | },
348 | "nanoid": {
349 | "version": "3.1.20",
350 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
351 | "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw=="
352 | },
353 | "node-emoji": {
354 | "version": "1.10.0",
355 | "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
356 | "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==",
357 | "requires": {
358 | "lodash.toarray": "^4.4.0"
359 | }
360 | },
361 | "node-releases": {
362 | "version": "1.1.70",
363 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz",
364 | "integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw=="
365 | },
366 | "normalize-range": {
367 | "version": "0.1.2",
368 | "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
369 | "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI="
370 | },
371 | "object-assign": {
372 | "version": "4.1.1",
373 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
374 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
375 | },
376 | "object-hash": {
377 | "version": "2.1.1",
378 | "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz",
379 | "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ=="
380 | },
381 | "once": {
382 | "version": "1.4.0",
383 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
384 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
385 | "requires": {
386 | "wrappy": "1"
387 | }
388 | },
389 | "path-is-absolute": {
390 | "version": "1.0.1",
391 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
392 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
393 | },
394 | "path-parse": {
395 | "version": "1.0.6",
396 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
397 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
398 | },
399 | "postcss": {
400 | "version": "8.2.6",
401 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.6.tgz",
402 | "integrity": "sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg==",
403 | "requires": {
404 | "colorette": "^1.2.1",
405 | "nanoid": "^3.1.20",
406 | "source-map": "^0.6.1"
407 | }
408 | },
409 | "postcss-functions": {
410 | "version": "3.0.0",
411 | "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz",
412 | "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=",
413 | "requires": {
414 | "glob": "^7.1.2",
415 | "object-assign": "^4.1.1",
416 | "postcss": "^6.0.9",
417 | "postcss-value-parser": "^3.3.0"
418 | },
419 | "dependencies": {
420 | "ansi-styles": {
421 | "version": "3.2.1",
422 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
423 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
424 | "requires": {
425 | "color-convert": "^1.9.0"
426 | }
427 | },
428 | "chalk": {
429 | "version": "2.4.2",
430 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
431 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
432 | "requires": {
433 | "ansi-styles": "^3.2.1",
434 | "escape-string-regexp": "^1.0.5",
435 | "supports-color": "^5.3.0"
436 | }
437 | },
438 | "color-convert": {
439 | "version": "1.9.3",
440 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
441 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
442 | "requires": {
443 | "color-name": "1.1.3"
444 | }
445 | },
446 | "color-name": {
447 | "version": "1.1.3",
448 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
449 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
450 | },
451 | "has-flag": {
452 | "version": "3.0.0",
453 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
454 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
455 | },
456 | "postcss": {
457 | "version": "6.0.23",
458 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
459 | "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
460 | "requires": {
461 | "chalk": "^2.4.1",
462 | "source-map": "^0.6.1",
463 | "supports-color": "^5.4.0"
464 | }
465 | },
466 | "postcss-value-parser": {
467 | "version": "3.3.1",
468 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
469 | "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
470 | },
471 | "supports-color": {
472 | "version": "5.5.0",
473 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
474 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
475 | "requires": {
476 | "has-flag": "^3.0.0"
477 | }
478 | }
479 | }
480 | },
481 | "postcss-js": {
482 | "version": "3.0.3",
483 | "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz",
484 | "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==",
485 | "requires": {
486 | "camelcase-css": "^2.0.1",
487 | "postcss": "^8.1.6"
488 | }
489 | },
490 | "postcss-nested": {
491 | "version": "5.0.3",
492 | "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.3.tgz",
493 | "integrity": "sha512-R2LHPw+u5hFfDgJG748KpGbJyTv7Yr33/2tIMWxquYuHTd9EXu27PYnKi7BxMXLtzKC0a0WVsqHtd7qIluQu/g==",
494 | "requires": {
495 | "postcss-selector-parser": "^6.0.4"
496 | }
497 | },
498 | "postcss-selector-parser": {
499 | "version": "6.0.4",
500 | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz",
501 | "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==",
502 | "requires": {
503 | "cssesc": "^3.0.0",
504 | "indexes-of": "^1.0.1",
505 | "uniq": "^1.0.1",
506 | "util-deprecate": "^1.0.2"
507 | }
508 | },
509 | "postcss-value-parser": {
510 | "version": "4.1.0",
511 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
512 | "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
513 | },
514 | "pretty-hrtime": {
515 | "version": "1.0.3",
516 | "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
517 | "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE="
518 | },
519 | "purgecss": {
520 | "version": "3.1.3",
521 | "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-3.1.3.tgz",
522 | "integrity": "sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ==",
523 | "requires": {
524 | "commander": "^6.0.0",
525 | "glob": "^7.0.0",
526 | "postcss": "^8.2.1",
527 | "postcss-selector-parser": "^6.0.2"
528 | }
529 | },
530 | "reduce-css-calc": {
531 | "version": "2.1.8",
532 | "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
533 | "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
534 | "requires": {
535 | "css-unit-converter": "^1.1.1",
536 | "postcss-value-parser": "^3.3.0"
537 | },
538 | "dependencies": {
539 | "postcss-value-parser": {
540 | "version": "3.3.1",
541 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
542 | "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
543 | }
544 | }
545 | },
546 | "resolve": {
547 | "version": "1.20.0",
548 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
549 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
550 | "requires": {
551 | "is-core-module": "^2.2.0",
552 | "path-parse": "^1.0.6"
553 | }
554 | },
555 | "simple-swizzle": {
556 | "version": "0.2.2",
557 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
558 | "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
559 | "requires": {
560 | "is-arrayish": "^0.3.1"
561 | }
562 | },
563 | "source-map": {
564 | "version": "0.6.1",
565 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
566 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
567 | },
568 | "supports-color": {
569 | "version": "7.2.0",
570 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
571 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
572 | "requires": {
573 | "has-flag": "^4.0.0"
574 | }
575 | },
576 | "tailwindcss": {
577 | "version": "2.0.3",
578 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.0.3.tgz",
579 | "integrity": "sha512-s8NEqdLBiVbbdL0a5XwTb8jKmIonOuI4RMENEcKLR61jw6SdKvBss7NWZzwCaD+ZIjlgmesv8tmrjXEp7C0eAQ==",
580 | "requires": {
581 | "@fullhuman/postcss-purgecss": "^3.1.3",
582 | "bytes": "^3.0.0",
583 | "chalk": "^4.1.0",
584 | "color": "^3.1.3",
585 | "detective": "^5.2.0",
586 | "didyoumean": "^1.2.1",
587 | "fs-extra": "^9.1.0",
588 | "html-tags": "^3.1.0",
589 | "lodash": "^4.17.20",
590 | "modern-normalize": "^1.0.0",
591 | "node-emoji": "^1.8.1",
592 | "object-hash": "^2.1.1",
593 | "postcss-functions": "^3",
594 | "postcss-js": "^3.0.3",
595 | "postcss-nested": "^5.0.1",
596 | "postcss-selector-parser": "^6.0.4",
597 | "postcss-value-parser": "^4.1.0",
598 | "pretty-hrtime": "^1.0.3",
599 | "reduce-css-calc": "^2.1.8",
600 | "resolve": "^1.19.0"
601 | }
602 | },
603 | "uniq": {
604 | "version": "1.0.1",
605 | "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
606 | "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8="
607 | },
608 | "universalify": {
609 | "version": "2.0.0",
610 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
611 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
612 | },
613 | "util-deprecate": {
614 | "version": "1.0.2",
615 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
616 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
617 | },
618 | "wrappy": {
619 | "version": "1.0.2",
620 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
621 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
622 | },
623 | "xtend": {
624 | "version": "4.0.2",
625 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
626 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
627 | }
628 | }
629 | }
630 |
--------------------------------------------------------------------------------