Refactoring/Enhancements/Fixes

Breaking Changes:
- Removed the `api` parameter; callbacks no longer need to be in a specific class.
- Topbar callbacks now take a chart as an argument (see updated callback examples)
- Removed the `topbar` parameter from chart declaration. The Topbar will be automatically created upon declaration of a topbar widget.
- Removed the `searchbox` parameter from charts. It will be created upon subscribing to it in `chart.events`.
- Removed `dynamic_loading`.
- Removed ‘volume_enabled’ parameter. Volume will be enabled if the volumn column is present in the dataframe.
- Widgets’ `func` parameter is now declared last.
- Switchers take a tuple of options rather than a variable number of arguments.
- `add_hotkey` renamed to `hotkey`
- Horizontal lines now take a `func` argument rather than `interactive`. This event will emit the Line object that was moved.
- Removed the `name` parameter from `line.set`. Line object names are now declared upon creation.

Enhancements:
- Added the `button` widget to the Topbar.
- Added the color picker to the drawing context menu.
- Charts now have a `candle_data` method, which returns the current data displayed on the chart as a DataFrame.
- Fixed callbacks are now located in the `chart.events` object:
    - search (e.g `chart.events.search += on_search`)
    - new_bar
    - range_change
- Added volume to the legend
- Drawings can now be accessed through `chart.toolbox.drawings`
- added the `style` and `name` parameters to `create_line`

Bug Fixes:
- Fixed a bug causing new charts not to load after `exit` was called.
- Refactored rayline placement to ensure they do not move the visible range.
- Fixed a bug causing the visible range to shift when trendlines are moved past the final candlestick.
- Fixed a bug preventing trendlines and raylines on irregular timeframes.
- Fixed a bug causing the legend to prevent mouse input into the chart.
This commit is contained in:
louisnw
2023-08-14 16:06:16 +01:00
parent 06b605d3a7
commit 34ce3f7199
22 changed files with 1024 additions and 784 deletions

View File

@ -1,6 +1,7 @@
import asyncio
import logging
import datetime as dt
import re
import threading
import queue
import json
@ -8,7 +9,6 @@ import ssl
from typing import Literal, Union, List
import pandas as pd
from lightweight_charts.util import _convert_timeframe
from lightweight_charts import Chart
try:
@ -21,6 +21,22 @@ except ImportError:
websockets = None
def convert_timeframe(timeframe):
spans = {
'min': 'minute',
'H': 'hour',
'D': 'day',
'W': 'week',
'M': 'month',
}
try:
multiplier = re.findall(r'\d+', timeframe)[0]
except IndexError:
return 1, spans[timeframe]
timespan = spans[timeframe.replace(multiplier, '')]
return multiplier, timespan
class PolygonAPI:
"""
Offers direct access to Polygon API data within all Chart objects.
@ -136,7 +152,7 @@ class PolygonAPI:
self._ws_q.put(('_unsubscribe', chart))
end_date = dt.datetime.now().strftime('%Y-%m-%d') if end_date == 'now' else end_date
mult, span = _convert_timeframe(timeframe)
mult, span = convert_timeframe(timeframe)
query_url = f"https://api.polygon.io/v2/aggs/ticker/{ticker.replace('-', '')}/range/{mult}/{span}/{start_date}/{end_date}?limit={limit}&apiKey={self._key}"
response = requests.get(query_url, headers={'User-Agent': 'lightweight_charts/1.0'})
@ -250,8 +266,11 @@ class PolygonAPI:
if sec_type == 'forex':
data['bp'] = data.pop('b')
data['ap'] = data.pop('a')
self._lasts[key]['price'] = (data['bp']+data['ap'])/2 if sec_type != 'indices' else data['val']
self._lasts[key]['volume'] = 0
if sec_type == 'indices':
self._lasts[key]['price'] = data['val']
else:
self._lasts[key]['price'] = (data['bp']+data['ap'])/2
self._lasts[key]['volume'] = 0
elif data['ev'] in ('A', 'CA', 'XA'):
self._lasts[key]['volume'] = data['v']
if not self._lasts[key].get('time'):
@ -305,8 +324,7 @@ class PolygonChart(Chart):
security_options: tuple = ('Stock', 'Option', 'Index', 'Forex', 'Crypto'),
toolbox: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
on_top: bool = False, maximize: bool = False, debug: bool = False):
super().__init__(volume_enabled=True, width=width, height=height, x=x, y=y, on_top=on_top, maximize=maximize, debug=debug,
api=self, topbar=True, searchbox=True, toolbox=toolbox)
super().__init__(width=width, height=height, x=x, y=y, on_top=on_top, maximize=maximize, debug=debug, toolbox=toolbox)
self.chart = self
self.num_bars = num_bars
self.end_date = end_date
@ -316,26 +334,18 @@ class PolygonChart(Chart):
self.topbar.active_background_color = 'rgb(91, 98, 246)'
self.topbar.textbox('symbol')
self.topbar.switcher('timeframe', self._on_timeframe_selection, *timeframe_options)
self.topbar.switcher('security', self._on_security_selection, *security_options)
self.topbar.switcher('timeframe', timeframe_options, func=self._on_timeframe_selection)
self.topbar.switcher('security', security_options, func=self._on_security_selection)
self.legend(True)
self.grid(False, False)
self.crosshair(vert_visible=False, horz_visible=False)
self.events.search += self.on_search
self.run_script(f'''
{self.id}.search.box.style.backgroundColor = 'rgba(91, 98, 246, 0.5)'
{self.id}.spinner.style.borderTop = '4px solid rgba(91, 98, 246, 0.8)'
{self.id}.search.window.style.display = "flex"
{self.id}.search.box.focus()
//let polyLogo = document.createElement('div')
//polyLogo.innerHTML = '<svg><g transform="scale(0.9)"><path d="M17.9821362,6 L24,12.1195009 L22.9236698,13.5060353 L17.9524621,27 L14.9907916,17.5798557 L12,12.0454987 L17.9821362,6 Z M21.437,15.304 L18.3670383,19.1065035 L18.367,23.637 L21.437,15.304 Z M18.203,7.335 L15.763,17.462 L17.595,23.287 L17.5955435,18.8249858 L22.963,12.176 L18.203,7.335 Z M17.297,7.799 L12.9564162,12.1857947 L15.228,16.389 L17.297,7.799 Z" fill="#FFFFFF"></path></g></svg>'
//polyLogo.style.position = 'absolute'
//polyLogo.style.width = '28px'
//polyLogo.style.zIndex = 10000
//polyLogo.style.right = '18px'
//polyLogo.style.top = '-1px'
//{self.id}.wrapper.appendChild(polyLogo)
''')
def _polygon(self, symbol):
@ -343,7 +353,7 @@ class PolygonChart(Chart):
self.set(pd.DataFrame(), True)
self.crosshair(vert_visible=False, horz_visible=False)
mult, span = _convert_timeframe(self.topbar['timeframe'].value)
mult, span = convert_timeframe(self.topbar['timeframe'].value)
delta = dt.timedelta(**{span + 's': int(mult)})
short_delta = (delta < dt.timedelta(days=7))
start_date = dt.datetime.now() if self.end_date == 'now' else dt.datetime.strptime(self.end_date, '%Y-%m-%d')
@ -367,18 +377,11 @@ class PolygonChart(Chart):
self.crosshair(vert_visible=True, horz_visible=True) if success else None
return success
async def on_search(self, searched_string): self.topbar['symbol'].set(searched_string if self._polygon(searched_string) else '')
async def on_search(self, chart, searched_string):
self.topbar['symbol'].set(searched_string if self._polygon(searched_string) else '')
async def _on_timeframe_selection(self):
async def _on_timeframe_selection(self, chart):
self._polygon(self.topbar['symbol'].value) if self.topbar['symbol'].value else None
async def _on_security_selection(self):
sec_type = self.topbar['security'].value
self.volume_enabled = False if sec_type == 'Index' else True
precision = 5 if sec_type == 'Forex' else 2
min_move = 1 / (10 ** precision) # 2 -> 0.1, 5 -> 0.00005 etc.
self.run_script(f'''
{self.chart.id}.series.applyOptions({{
priceFormat: {{precision: {precision}, minMove: {min_move}}}
}})''')
async def _on_security_selection(self, chart):
self.precision(5 if self.topbar['security'].value == 'Forex' else 2)