# piker: trading gear for hackers # Copyright (C) 2018-present Tyler Goodlet (in stewardship of pikers) # SPDX-License-Identifier: AGPL-3.0-or-later # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU Affero General Public License # as published by the Free Software Foundation, either version 3 # of the License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public # License along with this program. If not, see # . ''' Render an explicitly synthetic multiple-timeframe trend PNG. Run with ``--output /tmp/piker-trend-graphics.png``. This uses the actual shared tracker and snapshot graphics adapter. It launches no visible GUI, opens no broker or Jev connection, reads no API key, and isolates Qt/Piker configuration in a disposable directory. ''' import argparse import json import os from pathlib import Path from tempfile import TemporaryDirectory from types import SimpleNamespace def render( output: Path, *, periods: tuple[float, ...], order: int, samples: int, ) -> dict[str, object]: ''' Replay synthetic closes through one actual tracker and renderer. The final OHLC row remains mutable and is never submitted as a completed observation. Each timeframe retains real aggregation and TC state, while all plotted marker timestamps identify their final contributing base bar. Shading is deterministic TC output, not fabricated model inference. ''' import numpy as np import pyqtgraph as pg from piker.fsp._regime import MultiTimeframeTrend, RegimeSnapshot from piker.ui._trend_mtf import ( SnapshotTrendOverlay, _SnapshotLayer, ) from piker.ui.qt import QApplication app: QApplication = QApplication(['piker-trend-graphics']) app.setQuitOnLastWindowClosed(False) steps: np.ndarray = np.arange(samples) prices: np.ndarray = ( 100 + .009 * steps + 1.5 * np.sin(steps * .06) + .38 * np.sin(steps * .43) ) rows: np.ndarray = np.empty(samples, dtype=[ ('index', 'i8'), ('time', 'f8'), ('close', 'f8'), ('low', 'f8'), ('high', 'f8'), ]) rows['index'] = 1000 + steps rows['time'] = 1_700_000_100 + steps rows['close'] = prices rows['low'] = prices - .12 rows['high'] = prices + .12 shm: SimpleNamespace = SimpleNamespace( array=rows, _token='synthetic-demo-ohlcv', ) widget: pg.PlotWidget = pg.PlotWidget() widget.resize(1400, 760) widget.setBackground('#101923') widget.setTitle( 'SYNTHETIC: Trend Calculus across actual timeframes' '
' 'Larger triangles represent slower sampling periods; ' 'shading begins at confirmed regime detection.' '', color='#dce5ed', size='16pt', ) widget.plotItem.titleLabel.setFixedHeight(60) widget.showGrid(x=True, y=True, alpha=.13) widget.setLabel('left', 'Synthetic price') widget.setLabel('bottom', 'Absolute base-bar index (1 second)') widget.plot( rows['index'], prices, pen=pg.mkPen('#dce5ed', width=1.5), ) left: int = max(0, samples - 400) widget.setXRange(1000 + left, 1000 + samples, padding=0) widget.setYRange( float(prices[left:].min()) - .6, float(prices[left:].max()) + .6, padding=0, ) viz: SimpleNamespace = SimpleNamespace( plot=widget.plotItem, shm=shm, index_field='index', ) tracker: MultiTimeframeTrend = MultiTimeframeTrend( 'synthetic.demo', periods=periods, windows=(2, 2, 2, 2), history_limit=256, ) adapter: SnapshotTrendOverlay = SnapshotTrendOverlay( viz, shm, fqme='synthetic.demo', base_period=1, periods=periods, order=order, ) try: row: np.void snapshot: RegimeSnapshot for row in rows[:-1]: snapshot = tracker.push( float(row['time']), float(row['close']), index=int(row['index']), ) adapter.submit(snapshot) legend: pg.LegendItem = widget.addLegend(offset=(12, 12)) period: float layer: _SnapshotLayer for period, layer in adapter.layers.items(): legend.addItem( pg.ScatterPlotItem( symbol='t1', size=layer.markers.opts['size'], brush='#46c7a4', pen=pg.mkPen(None), ), f'{period:g}s / order {order}', ) widget.show() app.processEvents() output.parent.mkdir(parents=True, exist_ok=True) if not widget.grab().save(str(output)): raise RuntimeError('Qt could not save the requested PNG') return { 'mode': 'synthetic_native_graphics_no_model_calls', 'output': str(output), 'base_period': 1, 'periods': periods, 'order': order, 'finalized_bars': samples - 1, 'last_event_end': snapshot.as_of, 'layers': { period: { 'markers': len(layer.points), 'rectangles': len(layer.rectangles.intervals), } for period, layer in adapter.layers.items() }, } finally: adapter.remove() widget.close() widget.deleteLater() app.processEvents() def main() -> None: ''' Require an output path and isolate offscreen configuration. ''' parser: argparse.ArgumentParser = argparse.ArgumentParser( description=__doc__, ) parser.add_argument('--output', type=Path, required=True) parser.add_argument( '--periods', nargs='+', type=float, default=[1, 5, 15], ) parser.add_argument( '--order', type=int, choices=range(1, 5), default=1, ) parser.add_argument('--samples', type=int, default=600) args: argparse.Namespace = parser.parse_args() if not 120 <= args.samples <= 10_000: parser.error('--samples must be between 120 and 10000') prior: dict[str, str|None] = { 'QT_QPA_PLATFORM': os.environ.get('QT_QPA_PLATFORM'), 'XDG_CONFIG_HOME': os.environ.get('XDG_CONFIG_HOME'), } try: path: str with TemporaryDirectory(prefix='piker-trend-') as path: os.environ['QT_QPA_PLATFORM'] = 'offscreen' os.environ['XDG_CONFIG_HOME'] = path result: dict[str, object] = render( args.output.resolve(), periods=tuple(args.periods), order=args.order, samples=args.samples, ) print(json.dumps(result, indent=2)) finally: key: str value: str|None for key, value in prior.items(): if value is None: os.environ.pop(key, None) else: os.environ[key] = value if __name__ == '__main__': main()