236 lines
7.3 KiB
Python
236 lines
7.3 KiB
Python
# 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
|
|
# <https://www.gnu.org/licenses/>.
|
|
|
|
'''
|
|
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'
|
|
'<br><span style="font-size:11pt;color:#aab7c5">'
|
|
'Larger triangles represent slower sampling periods; '
|
|
'shading begins at confirmed regime detection.'
|
|
'</span>',
|
|
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()
|