8000 Add verified replay GIF and video with reproducible fixture provenance by jadenfix · Pull Request #7 · jadenfix/DRL4MM · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/demo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Replay demo
on:
push:
pull_request:
permissions:
contents: read
jobs:
reproduce:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python -m pip install numpy pandas gym==0.26.2 python-dotenv Pillow pytest
- run: sudo apt-get update && sudo apt-get install -y ffmpeg fonts-dejavu-core
- run: python -m pytest tests/test_accounting.py tests/test_historical_replay.py tests/test_lob_validation.py -q
- run: python demos/render_demo.py --output-dir /tmp/replay-demo
- uses: actions/upload-artifact@v4
with:
name: verified-software-fixture-demo
path: /tmp/replay-demo/
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Market-making research with a FIFO order book and a TD3 agent. The local
historical replay path consumes your own CSV order events, places quotes before
each next event, and exports fills plus reconciled cash, inventory, and PnL.

## Replay demo

![Rendered simulator replay using a labeled software fixture](demos/replay-demo.gif)

[MP4 video](demos/replay-demo.mp4) · [Reproduce and verify this capture](demos/README.md).
This visualizes actual CLI output from a synthetic software fixture; it is not
live market data or evidence of trading performance.

## Replay supplied market events

For replay alone, install the lightweight dependencies:
Expand Down
47 changes: 47 additions & 0 deletions demos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Replay visualization

This is a rendered visualization of the **real `simulator.replay` CLI output**,
not a screen recording of an interactive dashboard. Each displayed value comes
from one exported simulator state; animation time is slowed down for readability.

The bundled CSV is a deliberately small **synthetic software fixture**, not
historical market data. It exercises order matching and accounting. The chart
is not trading-performance evidence. No live exchange, fees, calibrated latency,
or trained policy is represented. This fixture is the same kind of explicit
software fixture used by the repository's accounting and replay regression tests.

![Real replay output, synthetic software fixture](replay-demo.gif)

[Download the 1280 x 720 H.264 MP4](replay-demo.mp4)

## Reproduce

From the repository root, using Python 3.12, ffmpeg and DejaVu Sans system fonts:

```sh
python -m pip install numpy pandas gym==0.26.2 python-dotenv Pillow pytest
python -m pytest tests/test_accounting.py tests/test_historical_replay.py tests/test_lob_validation.py -q
python demos/render_demo.py --output-dir /tmp/drl-replay-demo
```

On Debian/Ubuntu, `apt-get install ffmpeg fonts-dejavu-core` supplies the renderer
prerequisites. The generator executes the real CLI, requires full consumption,
checks both accounting identities for every row, and creates JSON, GIF, MP4 and
a still preview. The source manifest checks Git blob identities before rendering,
so source changes cannot silently retain the old source attribution. Refresh the
manifest and the source label only after revalidating changed source.

## Verified capture

- Simulator source: `81752dff261636ec96c940de82b22bfeff731fcb`.
- Exact fetched source files were verified against their Git blob SHA-1 values.
- Targeted accounting/replay/input-integrity regression command above: 41 passed.
- Real CLI: 38 of 38 events consumed, 36 steps, 36 fills, complete true.
- Final inventory: 0. Final fixture cash/equity: 3.60, up to floating-point rounding.
- Output JSON contains the fixture SHA-256 and all per-event accounting values.
- MP4 verified as H.264, yuv420p, 1280 x 720, 15 seconds; GIF 960 x 540.
- First, middle, and last rendered states inspected for legibility and fidelity.

The simulator source is unchanged by this demo PR. Optional training, live
integrations, and the unfinished legacy synthetic initializer remain outside
this capture's verification scope.
134 changes: 134 additions & 0 deletions demos/render_demo.py
8000
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Render actual replay JSON as labeled software-fixture media; no market claims."""
import argparse
import hashlib
import json
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile

from PIL import Image, ImageDraw, ImageFont

ROOT = Path(__file__).resolve().parents[1]
SOURCE = '81752dff261636ec96c940de82b22bfeff731fcb'
W, H, FPS, SECONDS = 1280, 720, 12, 15
BG, PANEL, INK, MUTED = '#0b1020', '#141e32', '#eef4ff', '#91a4c4'
TEAL, BLUE, AMBER = '#63efd1', '#7aabff', '#ffd088'


def font(size, bold=False):
"""Use a freely packaged system font, failing clearly if unavailable."""
suffix = '-Bold' if bold else ''
path = Path('/usr/share/fonts/truetype/dejavu') / f'DejaVuSans{suffix}.ttf'
return ImageFont.truetype(str(path), size)


def text(draw, xy, value, size=20, color=INK, bold=False):
"""Draw consistent screen typography."""
draw.text(xy, str(value), font=font(size, bold), fill=color)


def chart(draw, box, rows, key, title, color, bounds):
"""Plot only observed rows, with fixed axes over the full replay."""
x, y, width, height = box
draw.rounded_rectangle((x, y, x + width, y + height), 14, fill=PANEL)
text(draw, (x + 22, y + 16), title, 18, MUTED)
left, top, right, bottom = x + 65, y + 54, x + width - 24, y + height - 38
lo, hi = bounds
for i in range(4):
yy = top + (bottom - top) * i / 3
draw.line((left, yy, right, yy), fill='#27354f')
text(draw, (x + 15, yy - 8), f'{hi-(hi-lo)*i/3:.1f}', 13, MUTED)
points = [(left + (right-left)*(r['episode_steps']-1)/35,
bottom - (bottom-top)*(r[key]-lo)/(hi-lo)) for r in rows]
if len(points) > 1:
draw.line(points, fill=color, width=3)
if points:
px, py = points[-1]
draw.ellipse((px-5, py-5, px+5, py+5), fill=color)
text(draw, (left, bottom+12), 'Event 1', 12, MUTED)
text(draw, (right-70, bottom+12), 'Event 36', 12, MUTED)


def frame(report, index):
"""Present one exact simulator state; presentation time is slowed down."""
image = Image.new('RGB', (W, H), BG)
d = ImageDraw.Draw(image)
rows = report['rows'][:index+1]
row = rows[-1]
text(d, (40, 27), 'DRL4MM', 34, INK, True)
text(d, (253, 39), 'ORDER EVENT REPLAY', 17, TEAL, True)
d.rounded_rectangle((847, 29, 1240, 66), 10, fill='#2e2a24')
text(d, (861, 37), 'SOFTWARE FIXTURE / NO LIVE MARKET', 15, AMBER, True)
text(d, (40, 86), 'Follow the fills. Reconcile the book.', 29, INK, True)
text(d, (40, 129), 'CSV events > fixed quotes > FIFO matching > accounting JSON', 18, MUTED)
for x, name, value, unit in [(40, 'EVENTS CONSUMED', f'{index+3}/38', '2 initial book events'),
(346, 'EXECUTED FILLS', row['fill_count'], 'software fixture shares'),
(652, 'INVENTORY', row['inventory'], 'shares held'),
(958, 'RECONCILED EQUITY', f"{row['equity']:.2f}", 'fixture price units')]:
d.rounded_rectangle((x, 177, x+282, 290), 12, fill=PANEL)
text(d, (x+18, 192), name, 13, MUTED, True)
text(d, (x+18, 215), value, 31, TEAL if name=='INVENTORY' else INK, True)
text(d, (x+18, 260), unit, 12, MUTED)
chart(d, (40, 310, 585, 223), rows, 'inventory', 'Inventory after each event', TEAL, (-1, 3))
chart(d, (645, 310, 595, 223), rows, 'equity', 'Marked equity (fixture units)', BLUE, (-1, 5))
d.rounded_rectangle((40, 552, 1200+40, 661), 12, fill=PANEL)
text(d, (58, 565), 'LATEST EXECUTION', 13, MUTED, True)
fill = report['fills'][index]
text(d, (58, 590), f"{fill['side'].upper()} {fill['quantity']} @ {fill['price']:.2f}", 23, TEAL, True)
text(d, (352, 568), 'Cash + marked position = equity', 17, MUTED)
text(d, (352, 596), f"{row['cash']:.2f} + {row['position_value']:.2f} = {row['equity']:.2f}", 22, INK, True)
text(d, (886, 568), 'INPUT VERIFIED', 13, MUTED, True)
text(d, (886, 593), 'SHA-256 '+report['input_sha256'][:14], 16, MUTED)
text(d, (40, 677), 'No exchange, fees, calibrated latency, or trained-policy performance. Animation slows fixture replay.', 13, MUTED)
text(d, (1022, 677), 'Source '+SOURCE[:12], 12, MUTED)
return image


def main():
"""Execute real CLI, reconcile output, then encode GIF and H.264 video."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output-dir', type=Path, default=ROOT/'demos')
args = parser.parse_args()
if not shutil.which('ffmpeg'):
parser.error('ffmpeg is required')
manifest = json.loads((ROOT/'demos/source-manifest.json').read_text())
if manifest['source_commit'] != SOURCE:
parser.error('source label differs from manifest commit')
for relative, expected in manifest['git_blob_sha1'].items():
data = (ROOT/relative).read_bytes()
actual = hashlib.sha1(b'blob '+str(len(data)).encode()+b'\0'+data).hexdigest()
if actual != expected:
parser.error(f'{relative} differs from recorded source; refresh provenance before rendering')
out = args.output_dir.resolve()
out.mkdir(parents=True, exist_ok=True)
result = out/'replay.json'
subprocess.run([sys.executable, '-m', 'simulator.replay',
str(ROOT/'demos/software-fixture.csv'), '--output', str(result)],
cwd=ROOT, check=True)
report = json.loads(result.read_text())
assert report['complete'] and report['events_consumed'] == 38
assert len(report['rows']) == len(report['fills']) == 36
for row in report['rows']:
assert abs(row['equity']-row['cash']-row['position_value']) < 1e-8
assert abs(row['equity']-row['realized_pnl']-row['unrealized_pnl']) < 1e-8
with tempfile.TemporaryDirectory(prefix='drl-demo-') as temp:
directory = Path(temp)
for n in range(FPS*SECONDS):
# Hold first/last state; no interpolated or invented measurements.
index = max(0, min(35, int((n/FPS-1)*36/(SECONDS-3))))
frame(report, index).save(directory/f'{n:04d}.png')
common = ['ffmpeg', '-y', '-loglevel', 'error', '-framerate', str(FPS),
'-i', str(directory/'%04d.png')]
subprocess.run(common+['-c:v', 'libx264', '-crf', '22', '-pix_fmt', 'yuv420p',
'-movflags', '+faststart', str(out/'replay-demo.mp4')], check=True)
subprocess.run(common+['-filter_complex',
'fps=8,scale=960:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse',
'-loop', '0', str(out/'replay-demo.gif')], check=True)
frame(report, 35).save(out/'replay-preview.png')
print('Rendered real simulator output: replay-demo.mp4 and replay-demo.gif')


if __name__ == '__main__':
main()
Binary file added demos/replay-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/replay-demo.mp4
Binary file not shown.
Binary file added demos/replay-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading
0