Visualizing a dYdX Grid Market Maker’s Orders and Requotes
Summary
The document presents a plotting utility for examining a dYdX grid market maker run. It parses logs for mid-price requotes and order submission, acceptance, and cancellation events, then generates panels for theoretical grid levels around the mid-price, accepted-order lifetimes, order submissions grouped into short time buckets, and a schematic order-expiry timeline alongside block-height growth. The grid example uses three levels on either side of the mid-price, spaced at 100 basis points, and short-term orders with an eight-second expiry.
These plots help inspect how a configured grid relates to observed prices and how orders progress through their lifecycle. The order-lifetime chart only includes orders with both acceptance and cancellation events, while the timeline panel is illustrative rather than derived from log data. The document supplies no trading returns, fill-quality analysis, or comparison against alternative grid settings, so the panels are operational diagnostics rather than evidence of profitability.
Key ideas
- The utility extracts price and order-lifecycle events from a dYdX market-making log.
- One panel overlays a mid-price path with theoretical buy and sell grid levels.
- Order lifetime is measured from acceptance to cancellation for completed orders.
- Submission counts are aggregated into 250-millisecond buckets.
- The expiry and block-height timeline is schematic, and the document reports no profitability evidence.
Tags
Full text
# render_panels.py
```py
"""
Render the dYdX grid market maker tutorial panels from a captured live run.
After building NautilusTrader from source, run these commands from the repository root:
make sync
# Capture a live run (mainnet by default; for testnet, set the DYDX_NETWORK
# constant in node_grid_mm.rs to DydxNetwork::Testnet and rebuild).
timeout 35 ./target/release/examples/dydx-grid-mm > /tmp/dydx_main.log 2>&1
DYDX_LOG=/tmp/dydx_main.log \
uv run --project python --no-sync \
python docs/tutorials/assets/grid_market_maker_dydx/render_panels.py
The renderer parses ``Requoting`` lines for the mid trajectory and
``[SUBMIT_ORDER]`` / ``OrderAccepted`` / ``OrderCanceled`` events for the
order lifecycle, then writes four PNG panels using the ``nautilus_dark``
tearsheet theme.
"""
from __future__ import annotations
import os
import re
from pathlib import Path
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from nautilus_trader.analysis.tearsheet import _write_figure
from nautilus_trader.analysis.themes import get_theme
OUT = Path(__file__).resolve().parent
LOG_PATH = Path(os.environ.get("DYDX_LOG", "/tmp/dydx_main.log")) # noqa: S108
GRID_STEP_BPS = 100
NUM_LEVELS = 3
THEME = get_theme("nautilus_dark")
TEMPLATE = THEME["template"]
COLORS = THEME["colors"]
PRIMARY = COLORS["primary"]
POSITIVE = COLORS["positive"]
NEGATIVE = COLORS["negative"]
NEUTRAL = COLORS["neutral"]
GRID = COLORS["grid"]
ANSI = re.compile(r"\x1b\[[0-9;]*m")
TS = r"(?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)"
REQUOTE = re.compile(rf"{TS}.*Requoting grid: mid=(?P<mid>[\-0-9.]+)")
SUBMIT = re.compile(
rf"{TS}.*\[SUBMIT_ORDER\] Nautilus '(?P<id>[^']+)' .*side=(?P<side>Buy|Sell) qty=(?P<qty>[\-0-9.]+)",
)
ACCEPTED = re.compile(rf"{TS}.*OrderAccepted\(.*?client_order_id=(?P<id>[^,]+),")
CANCELED = re.compile(rf"{TS}.*OrderCanceled\(.*?client_order_id=(?P<id>[^,]+),")
def parse_log(path: Path) -> object:
requotes: list[dict] = []
submits: dict[str, dict] = {}
accepts: dict[str, pd.Timestamp] = {}
cancels: dict[str, pd.Timestamp] = {}
for raw in path.read_text(encoding="utf-8").splitlines():
line = ANSI.sub("", raw)
m = REQUOTE.search(line)
if m:
requotes.append(
{"ts": pd.Timestamp(m.group("ts")), "mid": float(m.group("mid"))},
)
continue
s = SUBMIT.search(line)
if s:
submits[s.group("id")] = {
"ts": pd.Timestamp(s.group("ts")),
"side": s.group("side").upper(),
"qty": float(s.group("qty")),
}
continue
a = ACCEPTED.search(line)
if a:
accepts[a.group("id")] = pd.Timestamp(a.group("ts"))
continue
c = CANCELED.search(line)
if c:
cancels[c.group("id")] = pd.Timestamp(c.group("ts"))
continue
return (
pd.DataFrame(requotes),
submits,
accepts,
cancels,
)
def apply_layout(fig: go.Figure, title: str, height: int = 480) -> None:
fig.update_layout(
template=TEMPLATE,
title={"text": title, "x": 0.02, "xanchor": "left"},
paper_bgcolor=COLORS["background"],
plot_bgcolor=COLORS["background"],
font={"family": "Inter, system-ui, sans-serif", "size": 13},
margin={"l": 60, "r": 30, "t": 70, "b": 50},
height=height,
width=1200,
legend={"orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "right", "x": 1.0},
)
fig.update_xaxes(gridcolor=GRID, zeroline=False)
fig.update_yaxes(gridcolor=GRID, zeroline=False)
def panel_a_grid_overlay(requotes: pd.DataFrame) -> go.Figure:
fig = go.Figure()
if requotes.empty:
apply_layout(fig, "No requote events captured", height=520)
return fig
df = requotes.copy().drop_duplicates(subset=["ts", "mid"]).reset_index(drop=True)
pct = GRID_STEP_BPS / 10_000.0
fig.add_trace(
go.Scatter(
x=df["ts"],
y=df["mid"],
mode="lines+markers",
name="Mid at requote",
line={"color": PRIMARY, "width": 1.6},
marker={"size": 6, "color": PRIMARY},
),
)
for level in range(1, NUM_LEVELS + 1):
fig.add_trace(
go.Scatter(
x=df["ts"],
y=df["mid"] * (1 - pct) ** level,
mode="lines",
name=f"Buy L{level}",
line={"color": POSITIVE, "width": 0.8, "dash": "dot"},
opacity=0.7,
),
)
fig.add_trace(
go.Scatter(
x=df["ts"],
y=df["mid"] * (1 + pct) ** level,
mode="lines",
name=f"Sell L{level}",
line={"color": NEGATIVE, "width": 0.8, "dash": "dot"},
opacity=0.7,
),
)
apply_layout(
fig,
f"ETH-USD-PERP mid at every requote with theoretical grid bands "
f"({GRID_STEP_BPS} bps step, {NUM_LEVELS} levels)",
height=520,
)
fig.update_yaxes(title_text="USD")
return fig
def panel_b_order_lifetime(submits, accepts, cancels) -> go.Figure:
fig = go.Figure()
rows: list[dict] = []
for cid, sub in submits.items():
if cid in accepts and cid in cancels:
rows.append(
{
"ts": accepts[cid],
"side": sub["side"],
"lifetime_secs": (cancels[cid] - accepts[cid]).total_seconds(),
},
)
if not rows:
apply_layout(fig, "Order lifetime distribution (no completed orders)", height=420)
return fig
df = pd.DataFrame(rows)
fig.add_trace(
go.Histogram(
x=df["lifetime_secs"],
nbinsx=30,
marker={"color": PRIMARY, "line": {"color": COLORS["background"], "width": 0.5}},
showlegend=False,
),
)
fig.add_vline(
x=8.0,
line={"color": NEGATIVE, "dash": "dash", "width": 1.2},
annotation_text="expire_time_secs = 8",
annotation_position="top right",
annotation={"font": {"size": 11, "color": NEGATIVE}},
)
apply_layout(
fig,
"Time from `OrderAccepted` to `OrderCanceled` per short-term order",
height=420,
)
fig.update_xaxes(title_text="seconds")
fig.update_yaxes(title_text="orders")
return fig
def panel_c_orders_per_cycle(submits, accepts) -> go.Figure:
fig = go.Figure()
if not submits:
apply_layout(fig, "Orders per requote cycle (no orders)", height=420)
return fig
rows = pd.DataFrame(
[
{
"ts": v["ts"],
"side": v["side"],
"accepted": k in accepts,
}
for k, v in submits.items()
],
)
rows["bucket"] = rows["ts"].dt.floor("250ms")
counts = (
rows.groupby("bucket")
.agg(
buys=("side", lambda s: (s == "BUY").sum()),
sells=("side", lambda s: (s == "SELL").sum()),
accepted=("accepted", "sum"),
)
.reset_index()
)
fig.add_trace(
go.Bar(
x=counts["bucket"],
y=counts["buys"],
name="Buy submissions",
marker={"color": POSITIVE},
),
)
fig.add_trace(
go.Bar(
x=counts["bucket"],
y=counts["sells"],
name="Sell submissions",
marker={"color": NEGATIVE},
),
)
fig.update_layout(barmode="stack")
apply_layout(fig, "Orders submitted per 250-ms bucket", height=420)
fig.update_xaxes(title_text="bucket")
fig.update_yaxes(title_text="orders")
return fig
def panel_d_short_term_timeline() -> go.Figure:
fig = make_subplots(
rows=2,
cols=1,
shared_xaxes=True,
vertical_spacing=0.10,
row_heights=[0.6, 0.4],
subplot_titles=(
"Short-term order time-to-live (expire_time_secs=8)",
"Block height growth (~0.5s per block)",
),
)
# Three orders submitted at t=0, 8, 16 (one per requote cycle).
for start in (0, 8, 16):
fig.add_trace(
go.Scatter(
x=[start, start + 8],
y=[8, 0],
mode="lines",
line={"color": PRIMARY, "width": 2.0},
showlegend=False,
),
row=1,
col=1,
)
fig.add_vline(x=start, line={"color": POSITIVE, "dash": "dot", "width": 1}, row=1, col=1)
fig.add_vline(
x=start + 8,
line={"color": NEGATIVE, "dash": "dot", "width": 1},
row=1,
col=1,
)
fig.add_hline(y=0, line={"color": NEUTRAL, "dash": "dash", "width": 1}, row=1, col=1)
# GoodTilBlock target moves with chain height. With 0.5s blocks, +16 blocks ~ 8s.
blocks = list(range(0, 51, 1))
fig.add_trace(
go.Scatter(
x=[b * 0.5 for b in blocks],
y=blocks,
mode="lines",
line={"color": PRIMARY, "width": 1.6},
showlegend=False,
),
row=2,
col=1,
)
apply_layout(
fig,
"Short-term order lifecycle: submit at t=0, 8, 16, expire 8 s later",
height=520,
)
fig.update_xaxes(title_text="seconds since first submission", row=2, col=1, range=[0, 25])
fig.update_yaxes(title_text="seconds remaining", range=[0, 9], row=1, col=1)
fig.update_yaxes(title_text="block height", row=2, col=1)
return fig
def main() -> None:
requotes, submits, accepts, cancels = parse_log(LOG_PATH)
print(
f"requotes={len(requotes)} submits={len(submits)} "
f"accepts={len(accepts)} cancels={len(cancels)}",
)
panels = {
"panel_a_grid_overlay.png": panel_a_grid_overlay(requotes),
"panel_b_order_lifetime.png": panel_b_order_lifetime(submits, accepts, cancels),
"panel_c_orders_per_cycle.png": panel_c_orders_per_cycle(submits, accepts),
"panel_d_short_term_timeline.png": panel_d_short_term_timeline(),
}
for name, fig in panels.items():
path = OUT / name
_write_figure(fig, str(path))
print(f"wrote {path} ({path.stat().st_size / 1024:.1f} KB)")
if __name__ == "__main__":
main()
```Shown in full with attribution under the source's licence. Licence: LGPL-3.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.