bn-lightweight-charts 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bn_lightweight_charts/__init__.py +7 -0
- bn_lightweight_charts/abstract.py +1001 -0
- bn_lightweight_charts/chart.py +241 -0
- bn_lightweight_charts/drawings.py +278 -0
- bn_lightweight_charts/js/bundle.dev.js +2472 -0
- bn_lightweight_charts/js/bundle.js +1 -0
- bn_lightweight_charts/js/index.html +25 -0
- bn_lightweight_charts/js/index_bn.html +144 -0
- bn_lightweight_charts/js/lightweight-charts.js +15475 -0
- bn_lightweight_charts/js/lightweight-charts.standalone.development.js +15475 -0
- bn_lightweight_charts/js/styles.css +257 -0
- bn_lightweight_charts/polygon.py +470 -0
- bn_lightweight_charts/table.py +138 -0
- bn_lightweight_charts/toolbox.py +45 -0
- bn_lightweight_charts/topbar.py +128 -0
- bn_lightweight_charts/util.py +227 -0
- bn_lightweight_charts/widgets.py +357 -0
- bn_lightweight_charts-0.2.0.dist-info/METADATA +317 -0
- bn_lightweight_charts-0.2.0.dist-info/RECORD +21 -0
- bn_lightweight_charts-0.2.0.dist-info/WHEEL +4 -0
- bn_lightweight_charts-0.2.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import datetime as dt
|
|
4
|
+
import re
|
|
5
|
+
import json
|
|
6
|
+
import urllib.request
|
|
7
|
+
from typing import Literal, Union, List
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from .chart import Chart
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import websockets
|
|
14
|
+
except ImportError:
|
|
15
|
+
websockets = None
|
|
16
|
+
|
|
17
|
+
SEC_TYPE = Literal['stocks', 'options', 'indices', 'forex', 'crypto']
|
|
18
|
+
|
|
19
|
+
ch = logging.StreamHandler()
|
|
20
|
+
ch.setFormatter(logging.Formatter('%(asctime)s | [polygon.io] %(levelname)s: %(message)s', datefmt='%H:%M:%S'))
|
|
21
|
+
ch.setLevel(logging.DEBUG)
|
|
22
|
+
_log = logging.getLogger('polygon')
|
|
23
|
+
_log.setLevel(logging.ERROR)
|
|
24
|
+
_log.addHandler(ch)
|
|
25
|
+
|
|
26
|
+
api_key = ''
|
|
27
|
+
_tickers = {}
|
|
28
|
+
_set_on_load = []
|
|
29
|
+
|
|
30
|
+
_lasts = {}
|
|
31
|
+
_ws = {'stocks': None, 'options': None, 'indices': None, 'crypto': None, 'forex': None}
|
|
32
|
+
_subscription_type = {
|
|
33
|
+
'stocks': ('Q', 'A'),
|
|
34
|
+
'options': ('Q', 'A'),
|
|
35
|
+
'indices': ('V', None),
|
|
36
|
+
'forex': ('C', 'CA'),
|
|
37
|
+
'crypto': ('XQ', 'XA'),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _convert_timeframe(timeframe):
|
|
42
|
+
spans = {
|
|
43
|
+
'min': 'minute',
|
|
44
|
+
'H': 'hour',
|
|
45
|
+
'D': 'day',
|
|
46
|
+
'W': 'week',
|
|
47
|
+
'M': 'month',
|
|
48
|
+
}
|
|
49
|
+
try:
|
|
50
|
+
multiplier = re.findall(r'\d+', timeframe)[0]
|
|
51
|
+
except IndexError:
|
|
52
|
+
return 1, spans[timeframe]
|
|
53
|
+
timespan = spans[timeframe.replace(multiplier, '')]
|
|
54
|
+
return multiplier, timespan
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_sec_type(ticker):
|
|
58
|
+
if '/' in ticker:
|
|
59
|
+
return 'forex'
|
|
60
|
+
for prefix, security_type in zip(('O:', 'I:', 'C:', 'X:'), ('options', 'indices', 'forex', 'crypto')):
|
|
61
|
+
if ticker.startswith(prefix):
|
|
62
|
+
return security_type
|
|
63
|
+
else:
|
|
64
|
+
return 'stocks'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _polygon_request(query_url):
|
|
68
|
+
query_url = 'https://api.polygon.io'+query_url
|
|
69
|
+
query_url += f'&apiKey={api_key}'
|
|
70
|
+
|
|
71
|
+
request = urllib.request.Request(query_url, headers={'User-Agent': 'lightweight_charts/1.0'})
|
|
72
|
+
with urllib.request.urlopen(request) as response:
|
|
73
|
+
if response.status != 200:
|
|
74
|
+
error = response.json()
|
|
75
|
+
_log.error(f'({response.status}) Request failed: {error["error"]}')
|
|
76
|
+
return
|
|
77
|
+
data = json.loads(response.read())
|
|
78
|
+
if 'results' not in data:
|
|
79
|
+
_log.error(f'No results for {query_url}')
|
|
80
|
+
return
|
|
81
|
+
return data['results']
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_bar_data(ticker: str, timeframe: str, start_date: str, end_date: str, limit: int = 5_000):
|
|
85
|
+
end_date = dt.datetime.now().strftime('%Y-%m-%d') if end_date == 'now' else end_date
|
|
86
|
+
mult, span = _convert_timeframe(timeframe)
|
|
87
|
+
if '-' in ticker:
|
|
88
|
+
ticker = ticker.replace('-', '')
|
|
89
|
+
|
|
90
|
+
query_url = f"/v2/aggs/ticker/{ticker}/range/{mult}/{span}/{start_date}/{end_date}?limit={limit}"
|
|
91
|
+
results = _polygon_request(query_url)
|
|
92
|
+
if not results:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
df = pd.DataFrame(results)
|
|
96
|
+
df['t'] = pd.to_datetime(df['t'], unit='ms')
|
|
97
|
+
|
|
98
|
+
rename = {'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 't': 'time'}
|
|
99
|
+
if not ticker.startswith('I:'):
|
|
100
|
+
rename['v'] = 'volume'
|
|
101
|
+
|
|
102
|
+
return df[rename.keys()].rename(columns=rename)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def async_get_bar_data(ticker: str, timeframe: str, start_date: str, end_date: str, limit: int = 5_000):
|
|
106
|
+
loop = asyncio.get_running_loop()
|
|
107
|
+
return await loop.run_in_executor(None, get_bar_data, ticker, timeframe, start_date, end_date, limit)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _send(sec_type: SEC_TYPE, action: str, params: str):
|
|
111
|
+
ws = _ws[sec_type]
|
|
112
|
+
while ws is None:
|
|
113
|
+
await asyncio.sleep(0.05)
|
|
114
|
+
ws = _ws[sec_type]
|
|
115
|
+
await ws.send(json.dumps({'action': action, 'params': params}))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def subscribe(ticker: str, sec_type: SEC_TYPE, func, args, precision=2):
|
|
119
|
+
if not _ws[sec_type]:
|
|
120
|
+
asyncio.create_task(_websocket_connect(sec_type))
|
|
121
|
+
|
|
122
|
+
if sec_type in ('forex', 'crypto'):
|
|
123
|
+
key = ticker[ticker.index(':')+1:]
|
|
124
|
+
key = key.replace('-', '/') if sec_type == 'forex' else key
|
|
125
|
+
else:
|
|
126
|
+
key = ticker
|
|
127
|
+
|
|
128
|
+
if not _lasts.get(key):
|
|
129
|
+
_lasts[key] = {
|
|
130
|
+
'price': 0,
|
|
131
|
+
'funcs': [],
|
|
132
|
+
'precision': precision
|
|
133
|
+
}
|
|
134
|
+
if sec_type != 'indices':
|
|
135
|
+
_lasts[key]['volume'] = 0
|
|
136
|
+
|
|
137
|
+
data = _lasts[key]
|
|
138
|
+
|
|
139
|
+
quotes, aggs = _subscription_type[sec_type]
|
|
140
|
+
await _send(sec_type, 'subscribe', f'{quotes}.{ticker}')
|
|
141
|
+
await _send(sec_type, 'subscribe', f'{aggs}.{ticker}') if aggs else None
|
|
142
|
+
|
|
143
|
+
if func in data['funcs']:
|
|
144
|
+
return
|
|
145
|
+
data['funcs'].append((func, args))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def unsubscribe(func):
|
|
149
|
+
for key, data in _lasts.items():
|
|
150
|
+
if val := next(((f, args) for f, args in data['funcs'] if f == func), None):
|
|
151
|
+
break
|
|
152
|
+
else:
|
|
153
|
+
return
|
|
154
|
+
data['funcs'].remove(val)
|
|
155
|
+
|
|
156
|
+
if data['funcs']:
|
|
157
|
+
return
|
|
158
|
+
sec_type = _get_sec_type(key)
|
|
159
|
+
quotes, aggs = _subscription_type[sec_type]
|
|
160
|
+
await _send(sec_type, 'unsubscribe', f'{quotes}.{key}')
|
|
161
|
+
await _send(sec_type, 'unsubscribe', f'{aggs}.{key}')
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def _websocket_connect(sec_type):
|
|
165
|
+
if websockets is None:
|
|
166
|
+
raise ImportError('The "websockets" library was not found, and must be installed to pull live data.')
|
|
167
|
+
ticker_key = {
|
|
168
|
+
'stocks': 'sym',
|
|
169
|
+
'options': 'sym',
|
|
170
|
+
'indices': 'T',
|
|
171
|
+
'forex': 'p',
|
|
172
|
+
'crypto': 'pair',
|
|
173
|
+
}[sec_type]
|
|
174
|
+
async with websockets.connect(f'wss://socket.polygon.io/{sec_type}') as ws:
|
|
175
|
+
_ws[sec_type] = ws
|
|
176
|
+
await _send(sec_type, 'auth', api_key)
|
|
177
|
+
while 1:
|
|
178
|
+
response = await ws.recv()
|
|
179
|
+
data_list: List[dict] = json.loads(response)
|
|
180
|
+
for i, data in enumerate(data_list):
|
|
181
|
+
if data['ev'] == 'status':
|
|
182
|
+
_log.info(f'{data["message"]}')
|
|
183
|
+
continue
|
|
184
|
+
_ticker_key = {
|
|
185
|
+
'stocks': 'sym',
|
|
186
|
+
'options': 'sym',
|
|
187
|
+
'indices': 'T',
|
|
188
|
+
'forex': 'p',
|
|
189
|
+
'crypto': 'pair',
|
|
190
|
+
}
|
|
191
|
+
await _handle_tick(data[ticker_key], data)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def _handle_tick(ticker, data):
|
|
195
|
+
lasts = _lasts[ticker]
|
|
196
|
+
sec_type = _get_sec_type(ticker)
|
|
197
|
+
|
|
198
|
+
if data['ev'] in ('Q', 'V', 'C', 'XQ'):
|
|
199
|
+
if sec_type == 'forex':
|
|
200
|
+
data['bp'] = data.pop('b')
|
|
201
|
+
data['ap'] = data.pop('a')
|
|
202
|
+
price = (data['bp'] + data['ap']) / 2 if sec_type != 'indices' else data['val']
|
|
203
|
+
if abs(price - lasts['price']) < (1/(10**lasts['precision'])):
|
|
204
|
+
return
|
|
205
|
+
lasts['price'] = price
|
|
206
|
+
|
|
207
|
+
if sec_type != 'indices':
|
|
208
|
+
lasts['volume'] = 0
|
|
209
|
+
|
|
210
|
+
if 't' not in data:
|
|
211
|
+
lasts['time'] = pd.to_datetime(data.pop('s'), unit='ms')
|
|
212
|
+
else:
|
|
213
|
+
lasts['time'] = pd.to_datetime(data['t'], unit='ms')
|
|
214
|
+
|
|
215
|
+
elif data['ev'] in ('A', 'CA', 'XA'):
|
|
216
|
+
lasts['volume'] = data['v']
|
|
217
|
+
if not lasts.get('time'):
|
|
218
|
+
return
|
|
219
|
+
lasts['symbol'] = ticker
|
|
220
|
+
for func, args in lasts['funcs']:
|
|
221
|
+
func(pd.Series(lasts), *args)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class PolygonAPI:
|
|
225
|
+
"""
|
|
226
|
+
Offers direct access to Polygon API data within all Chart objects.
|
|
227
|
+
|
|
228
|
+
It is not designed to be initialized by the user, and should be utilised
|
|
229
|
+
through the `polygon` method of `AbstractChart` (chart.polygon.<method>).
|
|
230
|
+
"""
|
|
231
|
+
_set_on_load = []
|
|
232
|
+
|
|
233
|
+
def __init__(self, chart):
|
|
234
|
+
self._chart = chart
|
|
235
|
+
|
|
236
|
+
def set(self, *args):
|
|
237
|
+
if asyncio.get_event_loop().is_running():
|
|
238
|
+
asyncio.create_task(self.async_set(*args))
|
|
239
|
+
return True
|
|
240
|
+
else:
|
|
241
|
+
_set_on_load.append(args)
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
async def async_set(self, sec_type: Literal['stocks', 'options', 'indices', 'forex', 'crypto'], ticker, timeframe,
|
|
245
|
+
start_date, end_date, limit, live):
|
|
246
|
+
await unsubscribe(self._chart.update_from_tick)
|
|
247
|
+
|
|
248
|
+
df = await async_get_bar_data(ticker, timeframe, start_date, end_date, limit)
|
|
249
|
+
|
|
250
|
+
self._chart.set(df, keep_drawings=_tickers.get(self._chart) == ticker)
|
|
251
|
+
_tickers[self._chart] = ticker
|
|
252
|
+
|
|
253
|
+
if not live:
|
|
254
|
+
return True
|
|
255
|
+
await subscribe(ticker, sec_type, self._chart.update_from_tick, (True,), self._chart.num_decimals)
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
def stock(
|
|
259
|
+
self, symbol: str, timeframe: str, start_date: str, end_date='now',
|
|
260
|
+
limit: int = 5_000, live: bool = False
|
|
261
|
+
) -> bool:
|
|
262
|
+
"""
|
|
263
|
+
Requests and displays stock data pulled from Polygon.io.\n
|
|
264
|
+
:param symbol: Ticker to request.
|
|
265
|
+
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
|
|
266
|
+
:param start_date: Start date of the data (YYYY-MM-DD).
|
|
267
|
+
:param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
|
|
268
|
+
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
|
|
269
|
+
:param live: If true, the data will be updated in real-time.
|
|
270
|
+
"""
|
|
271
|
+
return self.set('stocks', symbol, timeframe, start_date, end_date, limit, live)
|
|
272
|
+
|
|
273
|
+
def option(
|
|
274
|
+
self, symbol: str, timeframe: str, start_date: str, expiration: str = None,
|
|
275
|
+
right: Literal['C', 'P'] = None, strike: Union[int, float] = None,
|
|
276
|
+
end_date: str = 'now', limit: int = 5_000, live: bool = False
|
|
277
|
+
) -> bool:
|
|
278
|
+
"""
|
|
279
|
+
Requests and displays option data pulled from Polygon.io.\n
|
|
280
|
+
:param symbol: The underlying ticker to request.
|
|
281
|
+
A formatted option ticker can also be given instead of using the expiration, right, and strike parameters.
|
|
282
|
+
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
|
|
283
|
+
:param start_date: Start date of the data (YYYY-MM-DD).
|
|
284
|
+
:param expiration: Expiration of the option (YYYY-MM-DD).
|
|
285
|
+
:param right: Right of the option (C, P).
|
|
286
|
+
:param strike: The strike price of the option.
|
|
287
|
+
:param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
|
|
288
|
+
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
|
|
289
|
+
:param live: If true, the data will be updated in real-time.
|
|
290
|
+
"""
|
|
291
|
+
if any((expiration, right, strike)):
|
|
292
|
+
expiration = dt.datetime.strptime(expiration, "%Y-%m-%d").strftime("%y%m%d")
|
|
293
|
+
symbol = f'{symbol}{expiration}{right}{strike * 1000:08d}'
|
|
294
|
+
return self.set('options', f'O:{symbol}', timeframe, start_date, end_date, limit, live)
|
|
295
|
+
|
|
296
|
+
def index(
|
|
297
|
+
self, symbol: str, timeframe: str, start_date: str, end_date: str = 'now',
|
|
298
|
+
limit: int = 5_000, live: bool = False
|
|
299
|
+
) -> bool:
|
|
300
|
+
"""
|
|
301
|
+
Requests and displays index data pulled from Polygon.io.\n
|
|
302
|
+
:param symbol: Ticker to request.
|
|
303
|
+
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
|
|
304
|
+
:param start_date: Start date of the data (YYYY-MM-DD).
|
|
305
|
+
:param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
|
|
306
|
+
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
|
|
307
|
+
:param live: If true, the data will be updated in real-time.
|
|
308
|
+
"""
|
|
309
|
+
return self.set('indices', f'I:{symbol}', timeframe, start_date, end_date, limit, live)
|
|
310
|
+
|
|
311
|
+
def forex(
|
|
312
|
+
self, fiat_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
|
|
313
|
+
limit: int = 5_000, live: bool = False
|
|
314
|
+
) -> bool:
|
|
315
|
+
"""
|
|
316
|
+
Requests and displays forex data pulled from Polygon.io.\n
|
|
317
|
+
:param fiat_pair: The fiat pair to request. (USD-CAD, GBP-JPY etc.)
|
|
318
|
+
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
|
|
319
|
+
:param start_date: Start date of the data (YYYY-MM-DD).
|
|
320
|
+
:param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
|
|
321
|
+
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
|
|
322
|
+
:param live: If true, the data will be updated in real-time.
|
|
323
|
+
"""
|
|
324
|
+
return self.set('forex', f'C:{fiat_pair}', timeframe, start_date, end_date, limit, live)
|
|
325
|
+
|
|
326
|
+
def crypto(
|
|
327
|
+
self, crypto_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
|
|
328
|
+
limit: int = 5_000, live: bool = False
|
|
329
|
+
) -> bool:
|
|
330
|
+
"""
|
|
331
|
+
Requests and displays crypto data pulled from Polygon.io.\n
|
|
332
|
+
:param crypto_pair: The crypto pair to request. (BTC-USD, ETH-BTC etc.)
|
|
333
|
+
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
|
|
334
|
+
:param start_date: Start date of the data (YYYY-MM-DD).
|
|
335
|
+
:param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
|
|
336
|
+
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
|
|
337
|
+
:param live: If true, the data will be updated in real-time.
|
|
338
|
+
"""
|
|
339
|
+
return self.set('crypto', f'X:{crypto_pair}', timeframe, start_date, end_date, limit, live)
|
|
340
|
+
|
|
341
|
+
async def async_stock(
|
|
342
|
+
self, symbol: str, timeframe: str, start_date: str, end_date: str = 'now',
|
|
343
|
+
limit: int = 5_000, live: bool = False
|
|
344
|
+
) -> bool:
|
|
345
|
+
return await self.async_set('stocks', symbol, timeframe, start_date, end_date, limit, live)
|
|
346
|
+
|
|
347
|
+
async def async_option(
|
|
348
|
+
self, symbol: str, timeframe: str, start_date: str, expiration: str = None,
|
|
349
|
+
right: Literal['C', 'P'] = None, strike: Union[int, float] = None,
|
|
350
|
+
end_date: str = 'now', limit: int = 5_000, live: bool = False
|
|
351
|
+
) -> bool:
|
|
352
|
+
if any((expiration, right, strike)):
|
|
353
|
+
expiration = dt.datetime.strptime(expiration, "%Y-%m-%d").strftime("%y%m%d")
|
|
354
|
+
symbol = f'{symbol}{expiration}{right}{strike * 1000:08d}'
|
|
355
|
+
return await self.async_set('options', f'O:{symbol}', timeframe, start_date, end_date, limit, live)
|
|
356
|
+
|
|
357
|
+
async def async_index(
|
|
358
|
+
self, symbol: str, timeframe: str, start_date: str, end_date: str = 'now',
|
|
359
|
+
limit: int = 5_000, live: bool = False
|
|
360
|
+
) -> bool:
|
|
361
|
+
return await self.async_set('indices', f'I:{symbol}', timeframe, start_date, end_date, limit, live)
|
|
362
|
+
|
|
363
|
+
async def async_forex(
|
|
364
|
+
self, fiat_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
|
|
365
|
+
limit: int = 5_000, live: bool = False
|
|
366
|
+
) -> bool:
|
|
367
|
+
return await self.async_set('forex', f'C:{fiat_pair}', timeframe, start_date, end_date, limit, live)
|
|
368
|
+
|
|
369
|
+
async def async_crypto(
|
|
370
|
+
self, crypto_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
|
|
371
|
+
limit: int = 5_000, live: bool = False
|
|
372
|
+
) -> bool:
|
|
373
|
+
return await self.async_set('crypto', f'X:{crypto_pair}', timeframe, start_date, end_date, limit, live)
|
|
374
|
+
|
|
375
|
+
@staticmethod
|
|
376
|
+
def log(info: bool):
|
|
377
|
+
"""
|
|
378
|
+
Streams informational messages related to Polygon.io.
|
|
379
|
+
"""
|
|
380
|
+
_log.setLevel(logging.INFO) if info else _log.setLevel(logging.ERROR)
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def api_key(key: str):
|
|
384
|
+
"""
|
|
385
|
+
Sets the API key to be used with Polygon.io.
|
|
386
|
+
"""
|
|
387
|
+
global api_key
|
|
388
|
+
api_key = key
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class PolygonChart(Chart):
|
|
392
|
+
"""
|
|
393
|
+
A prebuilt callback chart object allowing for a standalone, plug-and-play
|
|
394
|
+
experience of Polygon.io's API.
|
|
395
|
+
|
|
396
|
+
Tickers, security types and timeframes are to be defined within the chart window.
|
|
397
|
+
|
|
398
|
+
If using the standard `show` method, the `block` parameter must be set to True.
|
|
399
|
+
`show_async` can also be used.
|
|
400
|
+
"""
|
|
401
|
+
def __init__(
|
|
402
|
+
self, api_key: str, live: bool = False, num_bars: int = 200, end_date: str = 'now', limit: int = 5_000,
|
|
403
|
+
timeframe_options: tuple = ('1min', '5min', '30min', 'D', 'W'),
|
|
404
|
+
security_options: tuple = ('Stock', 'Option', 'Index', 'Forex', 'Crypto'),
|
|
405
|
+
toolbox: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
|
|
406
|
+
on_top: bool = False, maximize: bool = False, debug: bool = False,
|
|
407
|
+
title: str = '', screen: int = None,
|
|
408
|
+
):
|
|
409
|
+
super().__init__(width, height, x, y, title, screen, on_top, maximize, debug, toolbox)
|
|
410
|
+
|
|
411
|
+
self.num_bars = num_bars
|
|
412
|
+
self.end_date = end_date
|
|
413
|
+
self.limit = limit
|
|
414
|
+
self.live = live
|
|
415
|
+
self.win.style(
|
|
416
|
+
active_background_color='rgba(91, 98, 246, 0.8)',
|
|
417
|
+
muted_background_color='rgba(91, 98, 246, 0.5)'
|
|
418
|
+
)
|
|
419
|
+
self.polygon.api_key(api_key)
|
|
420
|
+
self.events.search += self.on_search
|
|
421
|
+
self.legend(True)
|
|
422
|
+
self.grid(False, False)
|
|
423
|
+
self.crosshair(vert_visible=False, horz_visible=False)
|
|
424
|
+
|
|
425
|
+
self.topbar.textbox('symbol')
|
|
426
|
+
self.topbar.switcher('timeframe', timeframe_options, func=self._on_timeframe_selection)
|
|
427
|
+
self.topbar.switcher('security', security_options, func=self._on_security_selection)
|
|
428
|
+
|
|
429
|
+
self.run_script(f'''
|
|
430
|
+
{self.id}.search.window.style.display = "flex"
|
|
431
|
+
{self.id}.search.box.focus()
|
|
432
|
+
''')
|
|
433
|
+
|
|
434
|
+
async def _polygon(self, symbol):
|
|
435
|
+
self.spinner(True)
|
|
436
|
+
self.set(pd.DataFrame(), True)
|
|
437
|
+
self.crosshair(vert_visible=False, horz_visible=False)
|
|
438
|
+
|
|
439
|
+
mult, span = _convert_timeframe(self.topbar['timeframe'].value)
|
|
440
|
+
delta = dt.timedelta(**{span + 's': int(mult)})
|
|
441
|
+
short_delta = (delta < dt.timedelta(days=7))
|
|
442
|
+
start_date = dt.datetime.now() if self.end_date == 'now' else dt.datetime.strptime(self.end_date, '%Y-%m-%d')
|
|
443
|
+
remaining_bars = self.num_bars
|
|
444
|
+
while remaining_bars > 0:
|
|
445
|
+
start_date -= delta
|
|
446
|
+
if start_date.weekday() > 4 and short_delta: # Monday to Friday (0 to 4)
|
|
447
|
+
continue
|
|
448
|
+
remaining_bars -= 1
|
|
449
|
+
epoch = dt.datetime.fromtimestamp(0)
|
|
450
|
+
start_date = epoch if start_date < epoch else start_date
|
|
451
|
+
success = await getattr(self.polygon, 'async_'+self.topbar['security'].value.lower())(
|
|
452
|
+
symbol,
|
|
453
|
+
timeframe=self.topbar['timeframe'].value,
|
|
454
|
+
start_date=start_date.strftime('%Y-%m-%d'),
|
|
455
|
+
end_date=self.end_date,
|
|
456
|
+
limit=self.limit,
|
|
457
|
+
live=self.live
|
|
458
|
+
)
|
|
459
|
+
self.spinner(False)
|
|
460
|
+
self.crosshair() if success else None
|
|
461
|
+
return success
|
|
462
|
+
|
|
463
|
+
async def on_search(self, chart, searched_string):
|
|
464
|
+
chart.topbar['symbol'].set(searched_string if await self._polygon(searched_string) else '')
|
|
465
|
+
|
|
466
|
+
async def _on_timeframe_selection(self, chart):
|
|
467
|
+
await self._polygon(chart.topbar['symbol'].value) if chart.topbar['symbol'].value else None
|
|
468
|
+
|
|
469
|
+
async def _on_security_selection(self, chart):
|
|
470
|
+
self.precision(5 if chart.topbar['security'].value == 'Forex' else 2)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import random
|
|
3
|
+
from typing import Union, Optional, Callable
|
|
4
|
+
|
|
5
|
+
from .util import jbool, Pane, NUM
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Section(Pane):
|
|
9
|
+
def __init__(self, table, section_type):
|
|
10
|
+
super().__init__(table.win)
|
|
11
|
+
self._table = table
|
|
12
|
+
self.type = section_type
|
|
13
|
+
|
|
14
|
+
def __call__(self, number_of_text_boxes: int, func: Optional[Callable] = None):
|
|
15
|
+
if func is not None:
|
|
16
|
+
self.win.handlers[self.id] = lambda boxId: func(self._table, int(boxId))
|
|
17
|
+
self.run_script(f'''
|
|
18
|
+
{self._table.id}.makeSection("{self.id}", "{self.type}", {number_of_text_boxes}, {"true" if func else ""})
|
|
19
|
+
''')
|
|
20
|
+
|
|
21
|
+
def __setitem__(self, key, value):
|
|
22
|
+
self.run_script(f'{self._table.id}.{self.type}[{key}].innerText = "{value}"')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Row(dict):
|
|
26
|
+
def __init__(self, table, id, items):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.run_script = table.run_script
|
|
29
|
+
self._table = table
|
|
30
|
+
self.id = id
|
|
31
|
+
self.meta = {}
|
|
32
|
+
self.run_script(f'{self._table.id}.newRow("{self.id}", {jbool(table.return_clicked_cells)})')
|
|
33
|
+
for key, val in items.items():
|
|
34
|
+
self[key] = val
|
|
35
|
+
|
|
36
|
+
def __setitem__(self, column, value):
|
|
37
|
+
if isinstance(column, tuple):
|
|
38
|
+
[self.__setitem__(col, val) for col, val in zip(column, value)]
|
|
39
|
+
return
|
|
40
|
+
original_value = value
|
|
41
|
+
if column in self._table._formatters:
|
|
42
|
+
value = self._table._formatters[column].replace(self._table.VALUE, str(value))
|
|
43
|
+
self.run_script(f'{self._table.id}.updateCell("{self.id}", "{column}", "{value}")')
|
|
44
|
+
return super().__setitem__(column, original_value)
|
|
45
|
+
|
|
46
|
+
def background_color(self, column, color): self._style('backgroundColor', column, color)
|
|
47
|
+
|
|
48
|
+
def text_color(self, column, color): self._style('textColor', column, color)
|
|
49
|
+
|
|
50
|
+
def _style(self, style, column, arg):
|
|
51
|
+
self.run_script(f"{self._table.id}.styleCell({self.id}, '{column}', '{style}', '{arg}')")
|
|
52
|
+
|
|
53
|
+
def delete(self):
|
|
54
|
+
self.run_script(f"{self._table.id}.deleteRow('{self.id}')")
|
|
55
|
+
self._table.pop(self.id)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Table(Pane, dict):
|
|
59
|
+
VALUE = 'CELL__~__VALUE__~__PLACEHOLDER'
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
window,
|
|
64
|
+
width: NUM,
|
|
65
|
+
height: NUM,
|
|
66
|
+
headings: tuple,
|
|
67
|
+
widths: Optional[tuple] = None,
|
|
68
|
+
alignments: Optional[tuple] = None,
|
|
69
|
+
position='left',
|
|
70
|
+
draggable: bool = False,
|
|
71
|
+
background_color: str = '#121417',
|
|
72
|
+
border_color: str = 'rgb(70, 70, 70)',
|
|
73
|
+
border_width: int = 1,
|
|
74
|
+
heading_text_colors: Optional[tuple] = None,
|
|
75
|
+
heading_background_colors: Optional[tuple] = None,
|
|
76
|
+
return_clicked_cells: bool = False,
|
|
77
|
+
func: Optional[Callable] = None
|
|
78
|
+
):
|
|
79
|
+
dict.__init__(self)
|
|
80
|
+
Pane.__init__(self, window)
|
|
81
|
+
self._formatters = {}
|
|
82
|
+
self.headings = headings
|
|
83
|
+
self.is_shown = True
|
|
84
|
+
def wrapper(rId, cId=None):
|
|
85
|
+
if return_clicked_cells:
|
|
86
|
+
func(self[rId], cId)
|
|
87
|
+
else:
|
|
88
|
+
func(self[rId])
|
|
89
|
+
|
|
90
|
+
async def async_wrapper(rId, cId=None):
|
|
91
|
+
if return_clicked_cells:
|
|
92
|
+
await func(self[rId], cId)
|
|
93
|
+
else:
|
|
94
|
+
await func(self[rId])
|
|
95
|
+
|
|
96
|
+
self.win.handlers[self.id] = async_wrapper if asyncio.iscoroutinefunction(func) else wrapper
|
|
97
|
+
self.return_clicked_cells = return_clicked_cells
|
|
98
|
+
|
|
99
|
+
self.run_script(f'''
|
|
100
|
+
{self.id} = new Lib.Table(
|
|
101
|
+
{width},
|
|
102
|
+
{height},
|
|
103
|
+
{list(headings)},
|
|
104
|
+
{list(widths) if widths else []},
|
|
105
|
+
{list(alignments) if alignments else []},
|
|
106
|
+
'{position}',
|
|
107
|
+
{jbool(draggable)},
|
|
108
|
+
'{background_color}',
|
|
109
|
+
'{border_color}',
|
|
110
|
+
{border_width},
|
|
111
|
+
{list(heading_text_colors) if heading_text_colors else []},
|
|
112
|
+
{list(heading_background_colors) if heading_background_colors else []}
|
|
113
|
+
)''')
|
|
114
|
+
self.run_script(f'{self.id}.callbackName = "{self.id}"') if func else None
|
|
115
|
+
self.footer = Section(self, 'footer')
|
|
116
|
+
self.header = Section(self, 'header')
|
|
117
|
+
|
|
118
|
+
def new_row(self, *values, id=None) -> Row:
|
|
119
|
+
row_id = random.randint(0, 99_999_999) if not id else id
|
|
120
|
+
self[row_id] = Row(self, row_id, {heading: item for heading, item in zip(self.headings, values)})
|
|
121
|
+
return self[row_id]
|
|
122
|
+
|
|
123
|
+
def clear(self): self.run_script(f"{self.id}.clearRows()"), super().clear()
|
|
124
|
+
|
|
125
|
+
def get(self, __key: Union[int, str]) -> Row: return super().get(int(__key))
|
|
126
|
+
|
|
127
|
+
def __getitem__(self, item): return super().__getitem__(int(item))
|
|
128
|
+
|
|
129
|
+
def format(self, column: str, format_str: str): self._formatters[column] = format_str
|
|
130
|
+
|
|
131
|
+
def resize(self, width: NUM, height: NUM): self.run_script(f'{self.id}.reSize({width}, {height})')
|
|
132
|
+
|
|
133
|
+
def visible(self, visible: bool):
|
|
134
|
+
self.is_shown = visible
|
|
135
|
+
self.run_script(f"""
|
|
136
|
+
{self.id}._div.style.display = '{'flex' if visible else 'none'}'
|
|
137
|
+
{self.id}._div.{'add' if visible else 'remove'}EventListener('mousedown', {self.id}.onMouseDown)
|
|
138
|
+
""")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ToolBox:
|
|
5
|
+
def __init__(self, chart):
|
|
6
|
+
self.run_script = chart.run_script
|
|
7
|
+
self.id = chart.id
|
|
8
|
+
self._save_under = None
|
|
9
|
+
self.drawings = {}
|
|
10
|
+
chart.win.handlers[f'save_drawings{self.id}'] = self._save_drawings
|
|
11
|
+
self.run_script(f'{self.id}.createToolBox()')
|
|
12
|
+
|
|
13
|
+
def save_drawings_under(self, widget: 'Widget'):
|
|
14
|
+
"""
|
|
15
|
+
Drawings made on charts will be saved under the widget given. eg `chart.toolbox.save_drawings_under(chart.topbar['symbol'])`.
|
|
16
|
+
"""
|
|
17
|
+
self._save_under = widget
|
|
18
|
+
|
|
19
|
+
def load_drawings(self, tag: str):
|
|
20
|
+
"""
|
|
21
|
+
Loads and displays the drawings on the chart stored under the tag given.
|
|
22
|
+
"""
|
|
23
|
+
if not self.drawings.get(tag):
|
|
24
|
+
return
|
|
25
|
+
self.run_script(f'if ({self.id}.toolBox) {self.id}.toolBox.loadDrawings({json.dumps(self.drawings[tag])})')
|
|
26
|
+
|
|
27
|
+
def import_drawings(self, file_path):
|
|
28
|
+
"""
|
|
29
|
+
Imports a list of drawings stored at the given file path.
|
|
30
|
+
"""
|
|
31
|
+
with open(file_path, 'r') as f:
|
|
32
|
+
json_data = json.load(f)
|
|
33
|
+
self.drawings = json_data
|
|
34
|
+
|
|
35
|
+
def export_drawings(self, file_path):
|
|
36
|
+
"""
|
|
37
|
+
Exports the current list of drawings to the given file path.
|
|
38
|
+
"""
|
|
39
|
+
with open(file_path, 'w+') as f:
|
|
40
|
+
json.dump(self.drawings, f, indent=4)
|
|
41
|
+
|
|
42
|
+
def _save_drawings(self, drawings):
|
|
43
|
+
if not self._save_under:
|
|
44
|
+
return
|
|
45
|
+
self.drawings[self._save_under.value] = json.loads(drawings)
|