WIP resize sidepanes to master plot
							parent
							
								
									4d06502bc8
								
							
						
					
					
						commit
						7d00244e8b
					
				|  | @ -276,6 +276,8 @@ class ChartnPane(QFrame): | ||||||
|         hbox.setContentsMargins(0, 0, 0, 0) |         hbox.setContentsMargins(0, 0, 0, 0) | ||||||
|         hbox.setSpacing(3) |         hbox.setSpacing(3) | ||||||
| 
 | 
 | ||||||
|  |         # self.setMaximumWidth() | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class LinkedSplits(QWidget): | class LinkedSplits(QWidget): | ||||||
|     ''' |     ''' | ||||||
|  | @ -339,7 +341,8 @@ class LinkedSplits(QWidget): | ||||||
| 
 | 
 | ||||||
|     def set_split_sizes( |     def set_split_sizes( | ||||||
|         self, |         self, | ||||||
|         prop: float = 0.375  # proportion allocated to consumer subcharts |         # prop: float = 0.375,  # proportion allocated to consumer subcharts | ||||||
|  |         prop: float = 5/8, | ||||||
| 
 | 
 | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         '''Set the proportion of space allocated for linked subcharts. |         '''Set the proportion of space allocated for linked subcharts. | ||||||
|  | @ -450,7 +453,6 @@ class LinkedSplits(QWidget): | ||||||
|             self.xaxis = xaxis |             self.xaxis = xaxis | ||||||
| 
 | 
 | ||||||
|         qframe = ChartnPane(sidepane=sidepane, parent=self.splitter) |         qframe = ChartnPane(sidepane=sidepane, parent=self.splitter) | ||||||
| 
 |  | ||||||
|         cpw = ChartPlotWidget( |         cpw = ChartPlotWidget( | ||||||
| 
 | 
 | ||||||
|             # this name will be used to register the primary |             # this name will be used to register the primary | ||||||
|  | @ -522,10 +524,10 @@ class LinkedSplits(QWidget): | ||||||
|             # track by name |             # track by name | ||||||
|             self.subplots[name] = cpw |             self.subplots[name] = cpw | ||||||
| 
 | 
 | ||||||
|             if sidepane: |             # if sidepane: | ||||||
|                 # TODO: use a "panes" collection to manage this? |             #     # TODO: use a "panes" collection to manage this? | ||||||
|                 sidepane.setMinimumWidth(self.chart.sidepane.width()) |             #     qframe.setMaximumWidth(self.chart.sidepane.width()) | ||||||
|                 sidepane.setMaximumWidth(self.chart.sidepane.width()) |             #     qframe.setMinimumWidth(self.chart.sidepane.width()) | ||||||
| 
 | 
 | ||||||
|             self.splitter.addWidget(qframe) |             self.splitter.addWidget(qframe) | ||||||
| 
 | 
 | ||||||
|  | @ -537,6 +539,16 @@ class LinkedSplits(QWidget): | ||||||
| 
 | 
 | ||||||
|         return cpw |         return cpw | ||||||
| 
 | 
 | ||||||
|  |     def resize_sidepanes( | ||||||
|  |         self, | ||||||
|  |     ) -> None: | ||||||
|  |         '''Size all sidepanes based on the OHLC "main" plot. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         for name, cpw in self.subplots.items(): | ||||||
|  |             cpw.sidepane.setMinimumWidth(self.chart.sidepane.width()) | ||||||
|  |             cpw.sidepane.setMaximumWidth(self.chart.sidepane.width()) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class ChartPlotWidget(pg.PlotWidget): | class ChartPlotWidget(pg.PlotWidget): | ||||||
|     ''' |     ''' | ||||||
|  | @ -681,9 +693,9 @@ class ChartPlotWidget(pg.PlotWidget): | ||||||
|         """Return a range tuple for the bars present in view. |         """Return a range tuple for the bars present in view. | ||||||
|         """ |         """ | ||||||
|         l, r = self.view_range() |         l, r = self.view_range() | ||||||
|         a = self._arrays['ohlc'] |         array = self._arrays['ohlc'] | ||||||
|         lbar = max(l, a[0]['index']) |         lbar = max(l, array[0]['index']) | ||||||
|         rbar = min(r, a[-1]['index']) |         rbar = min(r, array[-1]['index']) | ||||||
|         return l, lbar, rbar, r |         return l, lbar, rbar, r | ||||||
| 
 | 
 | ||||||
|     def default_view( |     def default_view( | ||||||
|  | @ -991,22 +1003,19 @@ class ChartPlotWidget(pg.PlotWidget): | ||||||
|             a = self._arrays['ohlc'] |             a = self._arrays['ohlc'] | ||||||
|             ifirst = a[0]['index'] |             ifirst = a[0]['index'] | ||||||
|             bars = a[lbar - ifirst:rbar - ifirst + 1] |             bars = a[lbar - ifirst:rbar - ifirst + 1] | ||||||
| 
 |  | ||||||
|             if not len(bars): |             if not len(bars): | ||||||
|                 # likely no data loaded yet or extreme scrolling? |                 # likely no data loaded yet or extreme scrolling? | ||||||
|                 log.error(f"WTF bars_range = {lbar}:{rbar}") |                 log.error(f"WTF bars_range = {lbar}:{rbar}") | ||||||
|                 return |                 return | ||||||
| 
 | 
 | ||||||
|             # TODO: should probably just have some kinda attr mark |             if self.data_key != self.linked.symbol.key: | ||||||
|             # that determines this behavior based on array type |                 bars = a[self.data_key] | ||||||
|             try: |                 ylow = np.nanmin(bars) | ||||||
|  |                 yhigh = np.nanmax((bars)) | ||||||
|  |             else: | ||||||
|  |                 # just the std ohlc bars | ||||||
|                 ylow = np.nanmin(bars['low']) |                 ylow = np.nanmin(bars['low']) | ||||||
|                 yhigh = np.nanmax(bars['high']) |                 yhigh = np.nanmax(bars['high']) | ||||||
|             except (IndexError, ValueError): |  | ||||||
|                 # likely non-ohlc array? |  | ||||||
|                 bars = bars[self.name] |  | ||||||
|                 ylow = np.nanmin(bars) |  | ||||||
|                 yhigh = np.nanmax(bars) |  | ||||||
| 
 | 
 | ||||||
|         if set_range: |         if set_range: | ||||||
|             # view margins: stay within a % of the "true range" |             # view margins: stay within a % of the "true range" | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ | ||||||
| Real-time display tasks for charting / graphics. | Real-time display tasks for charting / graphics. | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
|  | from contextlib import asynccontextmanager | ||||||
| import time | import time | ||||||
| from typing import Any | from typing import Any | ||||||
| from types import ModuleType | from types import ModuleType | ||||||
|  | @ -264,7 +265,7 @@ async def chart_from_quotes( | ||||||
|                 last_mx, last_mn = mx, mn |                 last_mx, last_mn = mx, mn | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async def spawn_fsps( | async def fan_out_spawn_fsp_daemons( | ||||||
| 
 | 
 | ||||||
|     linkedsplits: LinkedSplits, |     linkedsplits: LinkedSplits, | ||||||
|     fsps: dict[str, str], |     fsps: dict[str, str], | ||||||
|  | @ -275,22 +276,21 @@ async def spawn_fsps( | ||||||
|     loglevel: str, |     loglevel: str, | ||||||
| 
 | 
 | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Start financial signal processing in subactor. |     '''Create financial signal processing sub-actors (under flat tree) | ||||||
|  |     for each entry in config and attach to local graphics update tasks. | ||||||
| 
 | 
 | ||||||
|     Pass target entrypoint and historical data. |     Pass target entrypoint and historical data. | ||||||
| 
 | 
 | ||||||
|     """ |     ''' | ||||||
| 
 |  | ||||||
|     linkedsplits.focus() |     linkedsplits.focus() | ||||||
| 
 | 
 | ||||||
|     uid = tractor.current_actor().uid |     uid = tractor.current_actor().uid | ||||||
| 
 | 
 | ||||||
|     # spawns sub-processes which execute cpu bound FSP code |     # spawns sub-processes which execute cpu bound FSP code | ||||||
|     async with tractor.open_nursery(loglevel=loglevel) as n: |     async with ( | ||||||
| 
 |         tractor.open_nursery() as n, | ||||||
|         # spawns local task that consume and chart data streams from |         trio.open_nursery() as ln, | ||||||
|         # sub-procs |     ): | ||||||
|         async with trio.open_nursery() as ln: |  | ||||||
| 
 | 
 | ||||||
|         # Currently we spawn an actor per fsp chain but |         # Currently we spawn an actor per fsp chain but | ||||||
|         # likely we'll want to pool them eventually to |         # likely we'll want to pool them eventually to | ||||||
|  | @ -339,45 +339,30 @@ async def spawn_fsps( | ||||||
|                 display_name, |                 display_name, | ||||||
|                 conf, |                 conf, | ||||||
|                 group_status_key, |                 group_status_key, | ||||||
|  |                 loglevel, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|     # blocks here until all fsp actors complete |     # blocks here until all fsp actors complete | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async def run_fsp( |  | ||||||
| 
 |  | ||||||
|     portal: tractor._portal.Portal, |  | ||||||
|     linkedsplits: LinkedSplits, |  | ||||||
|     brokermod: ModuleType, |  | ||||||
|     sym: str, |  | ||||||
|     src_shm: ShmArray, |  | ||||||
|     fsp_func_name: str, |  | ||||||
|     display_name: str, |  | ||||||
|     conf: dict[str, Any], |  | ||||||
|     group_status_key: str, |  | ||||||
| 
 |  | ||||||
| ) -> None: |  | ||||||
|     """FSP stream chart update loop. |  | ||||||
| 
 |  | ||||||
|     This is called once for each entry in the fsp |  | ||||||
|     config map. |  | ||||||
|     """ |  | ||||||
|     done = linkedsplits.window().status_bar.open_status( |  | ||||||
|         f'loading fsp, {display_name}..', |  | ||||||
|         group_key=group_status_key, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     # make sidepane config widget |  | ||||||
| class FspConfig(BaseModel): | class FspConfig(BaseModel): | ||||||
| 
 |  | ||||||
|     class Config: |     class Config: | ||||||
|         validate_assignment = True |         validate_assignment = True | ||||||
| 
 | 
 | ||||||
|     name: str |     name: str | ||||||
|     period: int |     period: int | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | @asynccontextmanager | ||||||
|  | async def open_sidepane( | ||||||
|  | 
 | ||||||
|  |     linked: LinkedSplits, | ||||||
|  |     display_name: str, | ||||||
|  | 
 | ||||||
|  | ) -> FspConfig: | ||||||
|  | 
 | ||||||
|     sidepane: FieldsForm = mk_form( |     sidepane: FieldsForm = mk_form( | ||||||
|         parent=linkedsplits.godwidget, |         parent=linked.godwidget, | ||||||
|         fields_schema={ |         fields_schema={ | ||||||
|             'name': { |             'name': { | ||||||
|                 'label': '**fsp**:', |                 'label': '**fsp**:', | ||||||
|  | @ -386,6 +371,8 @@ async def run_fsp( | ||||||
|                     f'{display_name}' |                     f'{display_name}' | ||||||
|                 ], |                 ], | ||||||
|             }, |             }, | ||||||
|  | 
 | ||||||
|  |             # TODO: generate this from input map | ||||||
|             'period': { |             'period': { | ||||||
|                 'label': '**period**:', |                 'label': '**period**:', | ||||||
|                 'type': 'edit', |                 'type': 'edit', | ||||||
|  | @ -403,10 +390,46 @@ async def run_fsp( | ||||||
|         print(f'{key}: {value}') |         print(f'{key}: {value}') | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
|  |     # TODO: | ||||||
|  |     async with ( | ||||||
|  |         open_form_input_handling( | ||||||
|  |             sidepane, | ||||||
|  |             focus_next=linked.godwidget, | ||||||
|  |             on_value_change=settings_change, | ||||||
|  |         ) | ||||||
|  |     ): | ||||||
|  |         yield sidepane | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def run_fsp( | ||||||
|  | 
 | ||||||
|  |     portal: tractor._portal.Portal, | ||||||
|  |     linkedsplits: LinkedSplits, | ||||||
|  |     brokermod: ModuleType, | ||||||
|  |     sym: str, | ||||||
|  |     src_shm: ShmArray, | ||||||
|  |     fsp_func_name: str, | ||||||
|  |     display_name: str, | ||||||
|  |     conf: dict[str, Any], | ||||||
|  |     group_status_key: str, | ||||||
|  |     loglevel: str, | ||||||
|  | 
 | ||||||
|  | ) -> None: | ||||||
|  |     '''FSP stream chart update loop. | ||||||
|  | 
 | ||||||
|  |     This is called once for each entry in the fsp | ||||||
|  |     config map. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     done = linkedsplits.window().status_bar.open_status( | ||||||
|  |         f'loading fsp, {display_name}..', | ||||||
|  |         group_key=group_status_key, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|     async with ( |     async with ( | ||||||
|         portal.open_stream_from( |         portal.open_stream_from( | ||||||
| 
 | 
 | ||||||
|             # subactor entrypoint |             # chaining entrypoint | ||||||
|             fsp.cascade, |             fsp.cascade, | ||||||
| 
 | 
 | ||||||
|             # name as title of sub-chart |             # name as title of sub-chart | ||||||
|  | @ -415,15 +438,14 @@ async def run_fsp( | ||||||
|             dst_shm_token=conf['shm'].token, |             dst_shm_token=conf['shm'].token, | ||||||
|             symbol=sym, |             symbol=sym, | ||||||
|             fsp_func_name=fsp_func_name, |             fsp_func_name=fsp_func_name, | ||||||
|  |             loglevel=loglevel, | ||||||
| 
 | 
 | ||||||
|         ) as stream, |         ) as stream, | ||||||
| 
 | 
 | ||||||
|         # TODO: |         open_sidepane( | ||||||
|         open_form_input_handling( |             linkedsplits, | ||||||
|             sidepane, |             display_name, | ||||||
|             focus_next=linkedsplits.godwidget, |         ) as sidepane, | ||||||
|             on_value_change=settings_change, |  | ||||||
|         ), |  | ||||||
|     ): |     ): | ||||||
| 
 | 
 | ||||||
|         # receive last index for processed historical |         # receive last index for processed historical | ||||||
|  | @ -472,7 +494,7 @@ async def run_fsp( | ||||||
|             # read from last calculated value |             # read from last calculated value | ||||||
|             array = shm.array |             array = shm.array | ||||||
| 
 | 
 | ||||||
|             # XXX: fsp func names are unique meaning we don't have |             # XXX: fsp func names must be unique meaning we don't have | ||||||
|             # duplicates of the underlying data even if multiple |             # duplicates of the underlying data even if multiple | ||||||
|             # sub-charts reference it under different 'named charts'. |             # sub-charts reference it under different 'named charts'. | ||||||
|             value = array[fsp_func_name][-1] |             value = array[fsp_func_name][-1] | ||||||
|  | @ -489,6 +511,8 @@ async def run_fsp( | ||||||
|             array_key=fsp_func_name |             array_key=fsp_func_name | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |         chart.linked.resize_sidepanes() | ||||||
|  | 
 | ||||||
|         # TODO: figure out if we can roll our own `FillToThreshold` to |         # TODO: figure out if we can roll our own `FillToThreshold` to | ||||||
|         # get brush filled polygons for OS/OB conditions. |         # get brush filled polygons for OS/OB conditions. | ||||||
|         # ``pg.FillBetweenItems`` seems to be one technique using |         # ``pg.FillBetweenItems`` seems to be one technique using | ||||||
|  | @ -622,6 +646,73 @@ async def check_for_new_bars(feed, ohlcv, linkedsplits): | ||||||
|             price_chart.increment_view() |             price_chart.increment_view() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def has_vlm(ohlcv: ShmArray) -> bool: | ||||||
|  |     # make sure that the instrument supports volume history | ||||||
|  |     # (sometimes this is not the case for some commodities and | ||||||
|  |     # derivatives) | ||||||
|  |     volm = ohlcv.array['volume'] | ||||||
|  |     return not bool(np.all(np.isin(volm, -1)) or np.all(np.isnan(volm))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @asynccontextmanager | ||||||
|  | async def maybe_open_vlm_display( | ||||||
|  | 
 | ||||||
|  |     linked: LinkedSplits, | ||||||
|  |     ohlcv: ShmArray, | ||||||
|  | 
 | ||||||
|  | ) -> ChartPlotWidget: | ||||||
|  | 
 | ||||||
|  |     # make sure that the instrument supports volume history | ||||||
|  |     # (sometimes this is not the case for some commodities and | ||||||
|  |     # derivatives) | ||||||
|  |     # volm = ohlcv.array['volume'] | ||||||
|  |     # if ( | ||||||
|  |     #     np.all(np.isin(volm, -1)) or | ||||||
|  |     #     np.all(np.isnan(volm)) | ||||||
|  |     # ): | ||||||
|  |     if not has_vlm(ohlcv): | ||||||
|  |         log.warning(f"{linked.symbol.key} does not seem to have volume info") | ||||||
|  |     else: | ||||||
|  |         async with open_sidepane(linked, 'volume') as sidepane: | ||||||
|  |             # built-in $vlm | ||||||
|  |             shm = ohlcv | ||||||
|  |             chart = linked.add_plot( | ||||||
|  |                 name='vlm', | ||||||
|  |                 array=shm.array, | ||||||
|  | 
 | ||||||
|  |                 array_key='volume', | ||||||
|  |                 sidepane=sidepane, | ||||||
|  | 
 | ||||||
|  |                 # curve by default | ||||||
|  |                 ohlc=False, | ||||||
|  | 
 | ||||||
|  |                 # vertical bars | ||||||
|  |                 # stepMode=True, | ||||||
|  |                 # static_yrange=(0, 100), | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             # XXX: ONLY for sub-chart fsps, overlays have their | ||||||
|  |             # data looked up from the chart's internal array set. | ||||||
|  |             # TODO: we must get a data view api going STAT!! | ||||||
|  |             chart._shm = shm | ||||||
|  | 
 | ||||||
|  |             # should **not** be the same sub-chart widget | ||||||
|  |             assert chart.name != linked.chart.name | ||||||
|  | 
 | ||||||
|  |             # sticky only on sub-charts atm | ||||||
|  |             last_val_sticky = chart._ysticks[chart.name] | ||||||
|  | 
 | ||||||
|  |             # read from last calculated value | ||||||
|  |             value = shm.array['volume'][-1] | ||||||
|  | 
 | ||||||
|  |             last_val_sticky.update_from_data(-1, value) | ||||||
|  | 
 | ||||||
|  |             # size view to data once at outset | ||||||
|  |             chart._set_yrange() | ||||||
|  | 
 | ||||||
|  |             yield chart | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| async def display_symbol_data( | async def display_symbol_data( | ||||||
| 
 | 
 | ||||||
|     godwidget: GodWidget, |     godwidget: GodWidget, | ||||||
|  | @ -686,6 +777,7 @@ async def display_symbol_data( | ||||||
|         # add as next-to-y-axis singleton pane |         # add as next-to-y-axis singleton pane | ||||||
|         godwidget.pp_pane = pp_pane |         godwidget.pp_pane = pp_pane | ||||||
| 
 | 
 | ||||||
|  |         # create main OHLC chart | ||||||
|         chart = linkedsplits.plot_ohlc_main( |         chart = linkedsplits.plot_ohlc_main( | ||||||
|             symbol, |             symbol, | ||||||
|             bars, |             bars, | ||||||
|  | @ -722,7 +814,7 @@ async def display_symbol_data( | ||||||
|                     'static_yrange': (0, 100), |                     'static_yrange': (0, 100), | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|             # test for duplicate fsps on same chart |             # # test for duplicate fsps on same chart | ||||||
|             # 'rsi2': { |             # 'rsi2': { | ||||||
|             #     'fsp_func_name': 'rsi', |             #     'fsp_func_name': 'rsi', | ||||||
|             #     'period': 14, |             #     'period': 14, | ||||||
|  | @ -733,18 +825,8 @@ async def display_symbol_data( | ||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         # make sure that the instrument supports volume history |         if has_vlm(ohlcv): | ||||||
|         # (sometimes this is not the case for some commodities and |             # add VWAP to fsp config for downstream loading | ||||||
|         # derivatives) |  | ||||||
|         volm = ohlcv.array['volume'] |  | ||||||
|         if ( |  | ||||||
|             np.all(np.isin(volm, -1)) or |  | ||||||
|             np.all(np.isnan(volm)) |  | ||||||
|         ): |  | ||||||
|             log.warning( |  | ||||||
|                 f"{sym} does not seem to have volume info," |  | ||||||
|                 " dropping volume signals") |  | ||||||
|         else: |  | ||||||
|             fsp_conf.update({ |             fsp_conf.update({ | ||||||
|                 'vwap': { |                 'vwap': { | ||||||
|                     'fsp_func_name': 'vwap', |                     'fsp_func_name': 'vwap', | ||||||
|  | @ -756,11 +838,10 @@ async def display_symbol_data( | ||||||
|         async with ( |         async with ( | ||||||
| 
 | 
 | ||||||
|             trio.open_nursery() as ln, |             trio.open_nursery() as ln, | ||||||
| 
 |  | ||||||
|         ): |         ): | ||||||
|             # load initial fsp chain (otherwise known as "indicators") |             # load initial fsp chain (otherwise known as "indicators") | ||||||
|             ln.start_soon( |             ln.start_soon( | ||||||
|                 spawn_fsps, |                 fan_out_spawn_fsp_daemons, | ||||||
|                 linkedsplits, |                 linkedsplits, | ||||||
|                 fsp_conf, |                 fsp_conf, | ||||||
|                 sym, |                 sym, | ||||||
|  | @ -787,6 +868,7 @@ async def display_symbol_data( | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             async with ( |             async with ( | ||||||
|  |                 maybe_open_vlm_display(linkedsplits, ohlcv), | ||||||
| 
 | 
 | ||||||
|                 open_order_mode( |                 open_order_mode( | ||||||
|                     feed, |                     feed, | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue