Skip to content

Commit edcd2aa

Browse files
smooth the synthetic event generator: tight band around a slow random walk, lower default rate (#23)
The original generator placed every new limit order at a uniformly random integer price in [95, 105] and ran the engine at 50,000 events per second. On the desktop bench this is fine; on the Twilight dashboard wired to the live Fly machine it looks like the top of book is glitching across 100 different values per second, because that is literally what is happening. Replace the price distribution with a mean-reverting random walk around a fixed anchor (kAnchor = 100). Each event nudges the reference by at most one tick, and the away-from-anchor step is suppressed when the reference has drifted off the anchor. New limit orders cluster within four ticks of the reference, so the resting book builds depth at a handful of adjacent levels rather than scattering across the whole price range. The event mix favors limit orders more heavily (80 percent limits, 8 percent market sweeps, 12 percent cancels) so the visible book accumulates depth between fills. Drop the default rate from 50000 to 400 events per second. The bench binary is the right place to push throughput; the live demo runs at a rate the React renderer can show legibly. Update fly.toml's MERIDIAN_RATE accordingly. Local smoke (60 frames over 10 s against the rebuilt server, seed 7): mid visited values: 99.5, 100.0, 100.5 bid/ask pairs alternate between (100,101), (99,101), (99,100) That is one to two ticks of organic movement, not flashing across the whole 10-tick range. 182/182 release tests still pass.
1 parent 7de1eb2 commit edcd2aa

2 files changed

Lines changed: 64 additions & 7 deletions

File tree

apps/server/main.cpp

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ namespace {
5151

5252
struct Cfg {
5353
int port = 0;
54-
std::uint64_t rate_eps = 50000; // engine events per second
54+
// Default rate is sized for a small Fly machine, not the bench. The
55+
// bench binary is the right place to push events per second; this
56+
// default keeps the live demo visually legible (the React dashboard
57+
// updates at 30 Hz and individual book moves should be readable).
58+
// Bump via --rate when running the server on a beefier host.
59+
std::uint64_t rate_eps = 400;
5560
std::uint64_t seed = 42;
5661
std::vector<std::string> origins; // empty = accept any (dev default)
5762
};
@@ -198,10 +203,25 @@ int main(int argc, char** argv) {
198203
std::fflush(stdout);
199204

200205
// Engine thread: synthetic limit / market / cancel mix.
206+
//
207+
// The price model tracks a slowly drifting reference mid: every event
208+
// nudges it by at most one tick with a strong pull toward the anchor
209+
// (kAnchor = 100), so the displayed top of book moves like a real
210+
// instrument rather than skipping uniformly across a wide range. New
211+
// limit orders cluster near the current reference (one to four ticks
212+
// off depending on side), so a tight visible spread builds and
213+
// moves a tick at a time. The event mix favors limit orders so the
214+
// book accumulates depth between cancels and market sweeps.
201215
std::thread engine_thr([&]() {
202216
std::mt19937_64 rng(cfg.seed);
203-
std::uniform_int_distribution<int> price_dist(95, 105);
204-
std::uniform_int_distribution<int> qty_dist(1, 30);
217+
constexpr int kAnchor = 100;
218+
constexpr int kPriceFloor = 92;
219+
constexpr int kPriceCeiling = 108;
220+
int reference = kAnchor;
221+
222+
std::uniform_int_distribution<int> offset_dist(0, 3); // ticks off the mid
223+
std::uniform_int_distribution<int> drift_dist(-1, 1); // small random walk
224+
std::uniform_int_distribution<int> qty_dist(1, 25);
205225
std::uniform_int_distribution<int> bucket(0, 99);
206226
std::bernoulli_distribution side_dist(0.5);
207227

@@ -220,22 +240,54 @@ int main(int argc, char** argv) {
220240
ev.symbol = kSym;
221241
ev.ts = ++ts;
222242
ev.side = side_dist(rng) ? meridian::Side::Buy : meridian::Side::Sell;
243+
244+
// Mean-reverting random walk: each event nudges the
245+
// reference at most one tick. At the anchor the step is a
246+
// free uniform draw over {-1, 0, +1}; away from the anchor
247+
// the away-direction step is suppressed so the reference
248+
// drifts back toward the anchor over time. This keeps the
249+
// displayed top of book within a few ticks of kAnchor.
250+
const int raw_step = drift_dist(rng);
251+
const int pull = reference > kAnchor ? -1 : (reference < kAnchor ? 1 : 0);
252+
int step;
253+
if (pull == 0) {
254+
step = raw_step;
255+
} else if (raw_step == 0 || raw_step == pull) {
256+
step = raw_step;
257+
} else {
258+
step = 0;
259+
}
260+
reference += step;
261+
if (reference < kPriceFloor) reference = kPriceFloor;
262+
if (reference > kPriceCeiling) reference = kPriceCeiling;
263+
223264
const int b = bucket(rng);
224-
if (b < 70 || max_id == 0) {
265+
if (b < 80 || max_id == 0) {
266+
// Limit order placed near the reference, with a small
267+
// offset so the resting book builds depth across a
268+
// handful of ticks rather than at one price.
225269
ev.kind = meridian::EventKind::NewOrder;
226270
ev.type = meridian::OrderType::Limit;
227271
ev.order_id = next_id++;
228-
ev.price = price_dist(rng);
272+
const int offset = offset_dist(rng);
273+
ev.price = (ev.side == meridian::Side::Buy)
274+
? reference - offset
275+
: reference + offset;
229276
ev.qty = qty_dist(rng);
230277
max_id = ev.order_id;
231-
} else if (b < 90) {
278+
} else if (b < 88) {
279+
// Occasional market sweep that lifts the resting book.
232280
ev.kind = meridian::EventKind::NewOrder;
233281
ev.type = meridian::OrderType::Market;
234282
ev.order_id = next_id++;
235283
ev.price = 0;
236284
ev.qty = qty_dist(rng) / 4 + 1;
237285
max_id = ev.order_id;
238286
} else {
287+
// Cancellation of an existing order. Real exchanges
288+
// cancel far more orders than they fill; the 12 percent
289+
// share here is a deliberate undercount to keep the
290+
// visible book lively rather than empty.
239291
std::uniform_int_distribution<meridian::OrderId> cancel_dist(1, max_id);
240292
ev.kind = meridian::EventKind::Cancel;
241293
ev.order_id = cancel_dist(rng);

fly.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ primary_region = "iad"
2222

2323
[env]
2424
MERIDIAN_PORT = "8080"
25-
MERIDIAN_RATE = "50000"
25+
# The live demo runs at a visually legible rate, not a benchmark rate.
26+
# The bench binary is the right place to push events per second; this
27+
# value (400 events per second, ~13 per 30 Hz sampler tick) keeps the
28+
# dashboard lively without flooding the React renderer or the Fly
29+
# shared CPU machine.
30+
MERIDIAN_RATE = "400"
2631
MERIDIAN_SEED = "42"
2732
# The Origin allowlist for /ws upgrades. Browsers always send Origin;
2833
# the production lockdown rejects anything that is not the Cloudflare

0 commit comments

Comments
 (0)