WIP resize sidepanes to master plot
							parent
							
								
									4d06502bc8
								
							
						
					
					
						commit
						7d00244e8b
					
				|  | @ -276,6 +276,8 @@ class ChartnPane(QFrame): | |||
|         hbox.setContentsMargins(0, 0, 0, 0) | ||||
|         hbox.setSpacing(3) | ||||
| 
 | ||||
|         # self.setMaximumWidth() | ||||
| 
 | ||||
| 
 | ||||
| class LinkedSplits(QWidget): | ||||
|     ''' | ||||
|  | @ -339,7 +341,8 @@ class LinkedSplits(QWidget): | |||
| 
 | ||||
|     def set_split_sizes( | ||||
|         self, | ||||
|         prop: float = 0.375  # proportion allocated to consumer subcharts | ||||
|         # prop: float = 0.375,  # proportion allocated to consumer subcharts | ||||
|         prop: float = 5/8, | ||||
| 
 | ||||
|     ) -> None: | ||||
|         '''Set the proportion of space allocated for linked subcharts. | ||||
|  | @ -450,7 +453,6 @@ class LinkedSplits(QWidget): | |||
|             self.xaxis = xaxis | ||||
| 
 | ||||
|         qframe = ChartnPane(sidepane=sidepane, parent=self.splitter) | ||||
| 
 | ||||
|         cpw = ChartPlotWidget( | ||||
| 
 | ||||
|             # this name will be used to register the primary | ||||
|  | @ -522,10 +524,10 @@ class LinkedSplits(QWidget): | |||
|             # track by name | ||||
|             self.subplots[name] = cpw | ||||
| 
 | ||||
|             if sidepane: | ||||
|                 # TODO: use a "panes" collection to manage this? | ||||
|                 sidepane.setMinimumWidth(self.chart.sidepane.width()) | ||||
|                 sidepane.setMaximumWidth(self.chart.sidepane.width()) | ||||
|             # if sidepane: | ||||
|             #     # TODO: use a "panes" collection to manage this? | ||||
|             #     qframe.setMaximumWidth(self.chart.sidepane.width()) | ||||
|             #     qframe.setMinimumWidth(self.chart.sidepane.width()) | ||||
| 
 | ||||
|             self.splitter.addWidget(qframe) | ||||
| 
 | ||||
|  | @ -537,6 +539,16 @@ class LinkedSplits(QWidget): | |||
| 
 | ||||
|         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): | ||||
|     ''' | ||||
|  | @ -681,9 +693,9 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|         """Return a range tuple for the bars present in view. | ||||
|         """ | ||||
|         l, r = self.view_range() | ||||
|         a = self._arrays['ohlc'] | ||||
|         lbar = max(l, a[0]['index']) | ||||
|         rbar = min(r, a[-1]['index']) | ||||
|         array = self._arrays['ohlc'] | ||||
|         lbar = max(l, array[0]['index']) | ||||
|         rbar = min(r, array[-1]['index']) | ||||
|         return l, lbar, rbar, r | ||||
| 
 | ||||
|     def default_view( | ||||
|  | @ -991,22 +1003,19 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|             a = self._arrays['ohlc'] | ||||
|             ifirst = a[0]['index'] | ||||
|             bars = a[lbar - ifirst:rbar - ifirst + 1] | ||||
| 
 | ||||
|             if not len(bars): | ||||
|                 # likely no data loaded yet or extreme scrolling? | ||||
|                 log.error(f"WTF bars_range = {lbar}:{rbar}") | ||||
|                 return | ||||
| 
 | ||||
|             # TODO: should probably just have some kinda attr mark | ||||
|             # that determines this behavior based on array type | ||||
|             try: | ||||
|             if self.data_key != self.linked.symbol.key: | ||||
|                 bars = a[self.data_key] | ||||
|                 ylow = np.nanmin(bars) | ||||
|                 yhigh = np.nanmax((bars)) | ||||
|             else: | ||||
|                 # just the std ohlc bars | ||||
|                 ylow = np.nanmin(bars['low']) | ||||
|                 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: | ||||
|             # view margins: stay within a % of the "true range" | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ | |||
| Real-time display tasks for charting / graphics. | ||||
| 
 | ||||
| ''' | ||||
| from contextlib import asynccontextmanager | ||||
| import time | ||||
| from typing import Any | ||||
| from types import ModuleType | ||||
|  | @ -264,7 +265,7 @@ async def chart_from_quotes( | |||
|                 last_mx, last_mn = mx, mn | ||||
| 
 | ||||
| 
 | ||||
| async def spawn_fsps( | ||||
| async def fan_out_spawn_fsp_daemons( | ||||
| 
 | ||||
|     linkedsplits: LinkedSplits, | ||||
|     fsps: dict[str, str], | ||||
|  | @ -275,22 +276,21 @@ async def spawn_fsps( | |||
|     loglevel: str, | ||||
| 
 | ||||
| ) -> 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. | ||||
| 
 | ||||
|     """ | ||||
| 
 | ||||
|     ''' | ||||
|     linkedsplits.focus() | ||||
| 
 | ||||
|     uid = tractor.current_actor().uid | ||||
| 
 | ||||
|     # spawns sub-processes which execute cpu bound FSP code | ||||
|     async with tractor.open_nursery(loglevel=loglevel) as n: | ||||
| 
 | ||||
|         # spawns local task that consume and chart data streams from | ||||
|         # sub-procs | ||||
|         async with trio.open_nursery() as ln: | ||||
|     async with ( | ||||
|         tractor.open_nursery() as n, | ||||
|         trio.open_nursery() as ln, | ||||
|     ): | ||||
| 
 | ||||
|         # Currently we spawn an actor per fsp chain but | ||||
|         # likely we'll want to pool them eventually to | ||||
|  | @ -339,45 +339,30 @@ async def spawn_fsps( | |||
|                 display_name, | ||||
|                 conf, | ||||
|                 group_status_key, | ||||
|                 loglevel, | ||||
|             ) | ||||
| 
 | ||||
|     # 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: | ||||
|         validate_assignment = True | ||||
| 
 | ||||
|     name: str | ||||
|     period: int | ||||
| 
 | ||||
| 
 | ||||
| @asynccontextmanager | ||||
| async def open_sidepane( | ||||
| 
 | ||||
|     linked: LinkedSplits, | ||||
|     display_name: str, | ||||
| 
 | ||||
| ) -> FspConfig: | ||||
| 
 | ||||
|     sidepane: FieldsForm = mk_form( | ||||
|         parent=linkedsplits.godwidget, | ||||
|         parent=linked.godwidget, | ||||
|         fields_schema={ | ||||
|             'name': { | ||||
|                 'label': '**fsp**:', | ||||
|  | @ -386,6 +371,8 @@ async def run_fsp( | |||
|                     f'{display_name}' | ||||
|                 ], | ||||
|             }, | ||||
| 
 | ||||
|             # TODO: generate this from input map | ||||
|             'period': { | ||||
|                 'label': '**period**:', | ||||
|                 'type': 'edit', | ||||
|  | @ -403,10 +390,46 @@ async def run_fsp( | |||
|         print(f'{key}: {value}') | ||||
|         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 ( | ||||
|         portal.open_stream_from( | ||||
| 
 | ||||
|             # subactor entrypoint | ||||
|             # chaining entrypoint | ||||
|             fsp.cascade, | ||||
| 
 | ||||
|             # name as title of sub-chart | ||||
|  | @ -415,15 +438,14 @@ async def run_fsp( | |||
|             dst_shm_token=conf['shm'].token, | ||||
|             symbol=sym, | ||||
|             fsp_func_name=fsp_func_name, | ||||
|             loglevel=loglevel, | ||||
| 
 | ||||
|         ) as stream, | ||||
| 
 | ||||
|         # TODO: | ||||
|         open_form_input_handling( | ||||
|             sidepane, | ||||
|             focus_next=linkedsplits.godwidget, | ||||
|             on_value_change=settings_change, | ||||
|         ), | ||||
|         open_sidepane( | ||||
|             linkedsplits, | ||||
|             display_name, | ||||
|         ) as sidepane, | ||||
|     ): | ||||
| 
 | ||||
|         # receive last index for processed historical | ||||
|  | @ -472,7 +494,7 @@ async def run_fsp( | |||
|             # read from last calculated value | ||||
|             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 | ||||
|             # sub-charts reference it under different 'named charts'. | ||||
|             value = array[fsp_func_name][-1] | ||||
|  | @ -489,6 +511,8 @@ async def run_fsp( | |||
|             array_key=fsp_func_name | ||||
|         ) | ||||
| 
 | ||||
|         chart.linked.resize_sidepanes() | ||||
| 
 | ||||
|         # TODO: figure out if we can roll our own `FillToThreshold` to | ||||
|         # get brush filled polygons for OS/OB conditions. | ||||
|         # ``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() | ||||
| 
 | ||||
| 
 | ||||
| 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( | ||||
| 
 | ||||
|     godwidget: GodWidget, | ||||
|  | @ -686,6 +777,7 @@ async def display_symbol_data( | |||
|         # add as next-to-y-axis singleton pane | ||||
|         godwidget.pp_pane = pp_pane | ||||
| 
 | ||||
|         # create main OHLC chart | ||||
|         chart = linkedsplits.plot_ohlc_main( | ||||
|             symbol, | ||||
|             bars, | ||||
|  | @ -722,7 +814,7 @@ async def display_symbol_data( | |||
|                     'static_yrange': (0, 100), | ||||
|                 }, | ||||
|             }, | ||||
|             # test for duplicate fsps on same chart | ||||
|             # # test for duplicate fsps on same chart | ||||
|             # 'rsi2': { | ||||
|             #     'fsp_func_name': 'rsi', | ||||
|             #     'period': 14, | ||||
|  | @ -733,18 +825,8 @@ async def display_symbol_data( | |||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         # 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)) | ||||
|         ): | ||||
|             log.warning( | ||||
|                 f"{sym} does not seem to have volume info," | ||||
|                 " dropping volume signals") | ||||
|         else: | ||||
|         if has_vlm(ohlcv): | ||||
|             # add VWAP to fsp config for downstream loading | ||||
|             fsp_conf.update({ | ||||
|                 'vwap': { | ||||
|                     'fsp_func_name': 'vwap', | ||||
|  | @ -756,11 +838,10 @@ async def display_symbol_data( | |||
|         async with ( | ||||
| 
 | ||||
|             trio.open_nursery() as ln, | ||||
| 
 | ||||
|         ): | ||||
|             # load initial fsp chain (otherwise known as "indicators") | ||||
|             ln.start_soon( | ||||
|                 spawn_fsps, | ||||
|                 fan_out_spawn_fsp_daemons, | ||||
|                 linkedsplits, | ||||
|                 fsp_conf, | ||||
|                 sym, | ||||
|  | @ -787,6 +868,7 @@ async def display_symbol_data( | |||
|             ) | ||||
| 
 | ||||
|             async with ( | ||||
|                 maybe_open_vlm_display(linkedsplits, ohlcv), | ||||
| 
 | ||||
|                 open_order_mode( | ||||
|                     feed, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue