Files
resonance-engine/TRADE_LBM_DIFF_SPEC.md
T
2026-06-08 20:34:31 +07:00

482 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# trade_lbm_v1 \u2014 v5 \u2192 trade_v1 Kernel Diff-Spec
**Purpose.** Concrete, file-level diff from `khra_gixx_1024_v5.cu` to `trade_lbm_v1.cu`. No CUDA written yet; this is the spec the kernel author works against. Companion to `TRADE_LBM_ARCHITECTURE.md` v4.
**Base file.** `D:\resonance-engine\cuda\khra_gixx_1024_v5.cu` (1284 lines).
**New file.** `D:\resonance-engine\cuda\trade_lbm_v1.cu` (will start as a copy; sections marked KEEP retain identical code, ALTER replaces with new behaviour, ADD is brand new, REMOVE is deleted).
---
## Section A \u2014 What is kept verbatim (KEEP)
These are the parts of v5 that are pure LBM mechanics and have no market dependency. Copy unchanged:
| v5 lines | What | Why kept |
|----------|------|----------|
| 32\u201337 | `M_PI`, Q=9 | unchanged |
| 39\u201346 | `CUDA_CHECK` macro | unchanged |
| 48\u201352 | `d_cx`, `d_cy`, `d_w` constants | D2Q9 lattice constants, identical |
| 66\u201382 | `streaming_kernel` | streaming step is the same regardless of grid meaning |
| 121\u2013135 | `compute_rho` | macroscopic density same |
| 137\u2013175 | `compute_velocity_stress_v4` | macroscopic ux/uy/stress identical |
| 234\u2013410 | host helpers: `crc32_init`, `crc32_compute`, `save_checkpoint`, `load_checkpoint` | infrastructure |
| 413\u2013506 | telemetry ring buffer | infrastructure |
| 510\u2013575 | ZMQ resilience + JSON helpers | infrastructure |
| 747\u2013768 | crash_handler / shutdown_handler / signal wiring | infrastructure |
---
## Section B \u2014 What changes (ALTER)
### B.1 Grid dimensions (lines 35\u201336)
```diff
-#define NX 1024
-#define NY 1024
+#define NX 512 // price-tick columns (centred on rolling mid)
+#define NY 512 // time rows (row 0 = newest, NY-1 = oldest)
```
Per architecture \u00a72. Lower memory cost than 1024\u00b2 \u2014 NX*NY*Q*sizeof(float) = 9 MB vs 36 MB for v5.
### B.2 Device-side parameters block (lines 54\u201355)
```diff
-__device__ float d_khra_amp = 0.03f;
-__device__ float d_gixx_amp = 0.008f;
+__device__ float d_oi_drive = 0.0f; // slow large-scale forcing amp; 0 = inactive
+__device__ float d_flow_drive = 0.0f; // fast fine-scale forcing amp; 0 = inactive
+__device__ float d_tick_size = 0.5f; // USD per column; instrument-dependent
+__device__ float d_mid_price = 0.0f; // current mid (host updates on recenter)
+__device__ int d_recenter_drift = 0; // cumulative integer-column drift since boot
+
+// Per-column omega (replaces single global omega). Stored in device global memory,
+// not __constant__ because it's updated O(once per snapshot) which exceeds the
+// 64KB cbank limit budget.
+__device__ float d_omega_col[NX];
+
+// Per-column depth-weighted equilibrium target. Updated by host on book events.
+__device__ float d_rho_eq[NX]; // target density
+__device__ float d_ux_eq[NX]; // target u_x (bias from depth imbalance)
+// u_y_eq is a single scalar (constant upward streaming velocity), defined below.
+__device__ float d_uy_eq = 0.0f; // typically 0 in v1; reserved for time-axis tuning
```
### B.3 Wave forcing function (lines 57\u201364)
Rename and re-anchor to market-derived amplitudes. Wavelengths unchanged.
```diff
-__device__ float khra_gixx_wave_1024(int x, int y, int cycle) {
- float khra = sinf(2.0f*M_PI*x/128.0f + cycle*0.025f) *
- cosf(2.0f*M_PI*y/128.0f + cycle*0.015f) * d_khra_amp;
- float gixx = sinf(2.0f*M_PI*x/8.0f + cycle*0.4f) *
- cosf(2.0f*M_PI*y/8.0f + cycle*0.35f) * d_gixx_amp;
- float asymmetry_factor = 1.0f + sinf(cycle * 0.05f) * 0.5f;
- return khra + gixx * asymmetry_factor;
-}
+__device__ float market_wave(int x, int y, int cycle) {
+ // Large-scale OI-driven wave: \u03bb = NX/4 = 128 columns (matches v5 khra \u03bb).
+ float oi = sinf(2.0f*M_PI*x/(NX/4.0f) + cycle*0.025f) *
+ cosf(2.0f*M_PI*y/(NX/4.0f) + cycle*0.015f) * d_oi_drive;
+ // Fine-scale flow-driven wave: \u03bb = 8 columns (matches v5 gixx \u03bb).
+ float flow = sinf(2.0f*M_PI*x/8.0f + cycle*0.4f) *
+ cosf(2.0f*M_PI*y/8.0f + cycle*0.35f) * d_flow_drive;
+ // Drop the asymmetry_factor envelope \u2014 it was a v5 substrate artefact;
+ // market amplitudes already carry their own time variation via host updates.
+ return oi + flow;
+}
```
Note: `ky = kx / 2` (the 2:1 spatial anisotropy) is **removed**. That was a physics-lattice geometric choice; on a price-time grid it would inject unwanted bias. The collision kernel below applies the wave as a scalar density nudge, not as separately-anisotropic ux/uy nudges.
### B.4 Collision kernel (lines 83\u2013119) \u2014 **largest change**
This is the heart of the diff. Two structural changes:
1. Per-column omega (read from `d_omega_col[x]`) replaces scalar `omega` argument.
2. Equilibrium target uses `(d_rho_eq[x], d_ux_eq[x], d_uy_eq)` instead of the cell's local `(rho, ux, uy)`.
```diff
-__global__ void collide_kernel_khragixx(float* f, float omega, int cycle) {
+__global__ void collide_kernel_trade(float* f, int cycle) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
if (x >= NX || y >= NY) return;
int idx = (y * NX + x) * Q;
float rho = 0.0f, ux = 0.0f, uy = 0.0f;
for (int i = 0; i < Q; i++) {
rho += f[idx + i];
ux += f[idx + i] * d_cx[i];
uy += f[idx + i] * d_cy[i];
}
if (rho < 0.1f) rho = 0.1f;
if (rho > 10.0f) rho = 10.0f;
ux /= rho;
uy /= rho;
- float u_mag = sqrtf(ux*ux + uy*uy);
- if (u_mag > 0.25f) { ux = ux * 0.25f / u_mag; uy = uy * 0.25f / u_mag; }
- float kx = khra_gixx_wave_1024(x, y, cycle);
- float ky = kx * 0.5f;
- ux += kx;
- uy += ky;
- u_mag = sqrtf(ux*ux + uy*uy);
- if (u_mag > 0.25f) { ux = ux * 0.25f / u_mag; uy = uy * 0.25f / u_mag; }
- float u_sq = ux*ux + uy*uy;
- for (int i = 0; i < Q; i++) {
- float eu = d_cx[i]*ux + d_cy[i]*uy;
- float feq = d_w[i] * rho * (1.0f + 3.0f*eu + 4.5f*eu*eu - 1.5f*u_sq);
- f[idx + i] = f[idx + i] - omega * (f[idx + i] - feq);
- if (isnan(f[idx + i]) || isinf(f[idx + i])) f[idx + i] = d_w[i];
- }
+ // Mach safety on observed velocity
+ float u_mag = sqrtf(ux*ux + uy*uy);
+ if (u_mag > 0.25f) { ux = ux * 0.25f / u_mag; uy = uy * 0.25f / u_mag; }
+
+ // Apply market wave as a scalar density nudge (not a velocity push).
+ // CRITICAL: the wave modulates the EQUILIBRIUM TARGET, not the current cell
+ // state. It is added to d_rho_eq[x] to produce rho_target. Do NOT add it to
+ // the locally-observed `rho` above — that would push the field instead of
+ // shifting where the field wants to relax to, and would destroy the
+ // book-as-attractor property that justifies this whole architecture.
+ float wave = market_wave(x, y, cycle);
+ float rho_target = d_rho_eq[x] + wave;
+ if (rho_target < 0.1f) rho_target = 0.1f;
+ if (rho_target > 10.0f) rho_target = 10.0f;
+
+ // Equilibrium target from depth-weighted book.
+ float ux_eq = d_ux_eq[x];
+ float uy_eq = d_uy_eq; // global, typically 0
+ float ueq_sq = ux_eq*ux_eq + uy_eq*uy_eq;
+
+ // Per-column relaxation rate (from spread tier).
+ float omega_x = d_omega_col[x];
+ if (omega_x < 0.05f) omega_x = 0.05f;
+ if (omega_x > 1.99f) omega_x = 1.99f;
+
+ for (int i = 0; i < Q; i++) {
+ float eu = d_cx[i]*ux_eq + d_cy[i]*uy_eq;
+ float feq = d_w[i] * rho_target * (1.0f + 3.0f*eu + 4.5f*eu*eu - 1.5f*ueq_sq);
+ f[idx + i] = f[idx + i] - omega_x * (f[idx + i] - feq);
+ if (isnan(f[idx + i]) || isinf(f[idx + i])) f[idx + i] = d_w[i] * rho_target;
+ }
```
**Subtle but important:** `feq` is now computed against the *book-derived* (rho_target, ux_eq, uy_eq), not against the *local cell's observed* (rho, ux, uy). That is what makes the book itself the relaxation target. The local rho/ux/uy are only computed to satisfy Mach safety and to detect numerical blow-ups; they do not feed the equilibrium.
### B.5 Inject kernel (lines 180\u2013203) \u2014 renamed + price-aware
```diff
-__global__ void inject_density_kernel(float* f,
- float cx, float cy,
- float sigma, float strength) {
+// inject_trade_kernel: a trade event at (column, row=0, sigma_x ticks, signed_size)
+// Note: trades always land at row 0 (newest). Sigma now in ticks along X only;
+// Y spread is one row (the trade happens at one wallclock instant).
+__global__ void inject_trade_kernel(float* f,
+ int col_centre,
+ float sigma_x,
+ float signed_dRho)
+{
int x = blockIdx.x * blockDim.x + threadIdx.x;
- int y = blockIdx.y * blockDim.y + threadIdx.y;
if (x >= NX || y >= NY) return;
+ int y = 0; // always row 0
- float dx = (float)x - cx;
- float dy = (float)y - cy;
- if (dx > NX * 0.5f) dx -= NX;
- if (dx < -NX * 0.5f) dx += NX;
- if (dy > NY * 0.5f) dy -= NY;
- if (dy < -NY * 0.5f) dy += NY;
- float r2 = dx * dx + dy * dy;
- float inv_2sig2 = 1.0f / (2.0f * sigma * sigma);
- float delta_rho = strength * expf(-r2 * inv_2sig2);
+ // X-axis: clamp distance (no wrap \u2014 price columns are NOT periodic).
+ float dx = (float)x - (float)col_centre;
+ float inv_2sig2 = 1.0f / (2.0f * sigma_x * sigma_x);
+ float delta_rho = signed_dRho * expf(-dx * dx * inv_2sig2);
int f_idx = (y * NX + x) * Q;
for (int i = 0; i < Q; i++) f[f_idx + i] += d_w[i] * delta_rho;
}
```
Launch this kernel with grid = (NX/blockDim.x, 1) instead of full 2D \u2014 only one row is touched. Periodicity is dropped on X because price columns are bounded; trades outside the window go to `off_grid_mass` (host accumulator).
### B.6 Asymmetry helper (lines 205\u2013214) \u2014 rebased on `rho_eq`
```diff
-float calculate_asymmetry_magnifying(float* h_rho) {
+// Asymmetry now measured relative to the per-column rho_eq (depth target),
+// not against a uniform 1.0 background. This makes the metric meaningful
+// against a non-uniform equilibrium.
+float calculate_asymmetry_magnifying(float* h_rho, const float* h_rho_eq) {
float sum_sq = 0.0f;
- for (int i = 0; i < NX * NY; i++) {
- float dev = h_rho[i] - 1.0f;
+ for (int idx = 0; idx < NX * NY; idx++) {
+ int x = idx % NX;
+ float dev = h_rho[idx] - h_rho_eq[x];
sum_sq += dev * dev;
}
return (sum_sq / (NX * NY)) * 100.0f;
}
```
### B.7 Telemetry JSON (line ~1136)
```diff
-"\"coherence\":%.4f,\"asymmetry\":%.4f,"
+"\"coherence\":%.4f,\"asymmetry\":%.4f,\"regime_product\":%.4f,"
```
`regime_product = coherence * asymmetry` computed host-side at telemetry emit. Adds one float multiply and ~20 bytes per snapshot.
### B.8 Command handler (line 595, especially lines 617\u2013640 and 703\u2013740)
Replace `set_khra_amp` / `set_gixx_amp` / `inject_density` with new commands:
| New command | JSON shape | What it does |
|-------------|-----------|--------------|
| `set_oi_drive` | `{"cmd":"set_oi_drive","value":<float>}` | sets `d_oi_drive`, clamp [0, 0.2] |
| `set_flow_drive` | `{"cmd":"set_flow_drive","value":<float>}` | sets `d_flow_drive`, clamp [0, 0.1] |
| `set_omega_profile` | `{"cmd":"set_omega_profile","values":[NX floats]}` | bulk write `d_omega_col[]`, each clamped [0.05, 1.99] |
| `set_omega_col` | `{"cmd":"set_omega_col","col":<int>,"value":<float>}` | single-column update for cheap incremental refresh |
| `set_book` | `{"cmd":"set_book","bid":[NX floats],"ask":[NX floats]}` | bulk write depths; host derives `d_rho_eq` and `d_ux_eq` and uploads |
| `inject_trade` | `{"cmd":"inject_trade","price":<f>,"side":"buy"|"sell","size":<f>,"aggressor":<bool>}` | host converts price\u2192column, signs the size, optionally multiplies by TAKER_WEIGHT, calls inject_trade_kernel |
| `set_mid` | `{"cmd":"set_mid","price":<f>}` | manual recenter |
| `set_tick_size` | `{"cmd":"set_tick_size","value":<f>}` | rare; restart-equivalent |
Keep verbatim: `pause`, `resume`, `reset`, `snapshot`, `health_check`, `reset_equilibrium`, `stress_snapshot_now`, `export_timeseries`.
Remove entirely: `set_khra_amp`, `set_gixx_amp`, `inject_density`.
### B.9 Main loop (line 770+ and the cycle % 10 telemetry block at line 1050)
Minor changes:
1. Call `collide_kernel_trade<<<...>>>(d_f, cycle)` instead of `collide_kernel_khragixx<<<...>>>(d_f, h_omega, cycle)`.
2. Drop `h_omega` host scalar.
3. Add `h_rho_eq[NX]`, `h_ux_eq[NX]`, `h_omega_col[NX]`, `h_bid_depth[NX]`, `h_ask_depth[NX]` host arrays and a small helper `recompute_equilibrium_from_book()` that runs whenever `set_book` arrives. **Formula is fixed (see B.9.1 below); kernel author MUST NOT improvise.**
4. Add `off_grid_mass` host accumulator updated when `inject_trade` finds column out of range.
### B.9.1 `recompute_equilibrium_from_book()` formula — fixed
The book-to-equilibrium mapping is part of the architecture, not a kernel-author choice. Use exactly:
```c
// Constants (compile-time)
#define RHO_EQ_MIN 0.5f // floor for rho_eq band
#define RHO_EQ_MAX 2.0f // ceiling for rho_eq band
#define RHO_EQ_NOMINAL 1.0f // target mean across columns
#define MAX_UX_BIAS 0.10f // velocity-bias cap; well below Mach safety 0.25
#define DEPTH_EPS 1e-6f // divide-by-zero guard for empty columns
static void recompute_equilibrium_from_book(void) {
// 1. Mean total depth across columns — used as the normalisation constant
// so that rho_eq[x] is centred near RHO_EQ_NOMINAL regardless of
// absolute book size.
double total_sum = 0.0;
for (int x = 0; x < NX; x++) {
total_sum += (double)(h_bid_depth[x] + h_ask_depth[x]);
}
float mean_total = (float)(total_sum / (double)NX);
if (mean_total < DEPTH_EPS) mean_total = DEPTH_EPS;
float norm = mean_total / RHO_EQ_NOMINAL; // divisor that maps mean->1.0
// 2. Per-column rho_eq (clamped) and ux_eq (imbalance-driven).
for (int x = 0; x < NX; x++) {
float total = h_bid_depth[x] + h_ask_depth[x];
float rho_eq = total / norm;
if (rho_eq < RHO_EQ_MIN) rho_eq = RHO_EQ_MIN;
if (rho_eq > RHO_EQ_MAX) rho_eq = RHO_EQ_MAX;
h_rho_eq[x] = rho_eq;
float denom = total > DEPTH_EPS ? total : DEPTH_EPS;
float imbalance = (h_bid_depth[x] - h_ask_depth[x]) / denom; // [-1, 1]
h_ux_eq[x] = imbalance * MAX_UX_BIAS;
}
// 3. Upload both arrays in one DMA pair.
CUDA_CHECK(cudaMemcpyToSymbol(d_rho_eq, h_rho_eq, sizeof(h_rho_eq)));
CUDA_CHECK(cudaMemcpyToSymbol(d_ux_eq, h_ux_eq, sizeof(h_ux_eq)));
}
```
**Why these constants.**
- `RHO_EQ_NOMINAL = 1.0` matches the D2Q9 weight normalisation; equilibrium density of 1.0 is the natural LBM rest state.
- `RHO_EQ_MIN/MAX = 0.5 / 2.0` is a ~2× dynamic range — enough that a 4×-deeper-than-average column is visible, but small enough that no column ever pushes the BGK collision outside well-tested regimes.
- `MAX_UX_BIAS = 0.10` is **0.4× the Mach safety ceiling 0.25**. A 100% one-sided book (bid only) produces ux_eq = 0.10. The collision kernel's Mach clamp at 0.25 is then a hard fault detector, not a routine clip.
- The normalisation uses **mean total depth across columns**, not a fixed constant, so the equilibrium band auto-rescales to whatever absolute book size the venue is showing. This means BTC at 200 contracts/level and ETH at 2000 contracts/level both produce the same equilibrium magnitudes.
This function runs on the host on every `set_book` command, which architecture §3.5 caps at ≤1 Hz. Cost: O(NX) = O(512) floats per call — negligible.
---
## Section C \u2014 What is added (ADD)
### C.1 Recentre routine (host)
```c
// Called after set_mid or auto-trigger when |cumulative drift| > NX/16.
// Shifts the entire f array by `shift` integer columns (positive = field
// scrolls right). Implemented as a CUDA memcpy with offset; out-of-range
// columns re-initialised to local equilibrium.
static void recentre_field(float* d_f, int shift);
```
Trigger logic: host tracks `mid_price` and `mid_price_at_grid_centre`. When `|price - centre| > RECENTRE_THRESHOLD * tick_size`, call `recentre_field`, update `d_mid_price`, set `recenter_event` telemetry flag for one snapshot.
### C.2 Per-column profile telemetry kernel
```c
// Reduces NY rows down to per-column scalars for the profile observables
// listed in architecture \u00a75. One block per column, reduction across Y.
__global__ void compute_column_profiles(
const float* rho, const float* ux, const float* uy,
const float* sxx, const float* sxy,
float* density_profile, // [NX]
float* vorticity_profile, // [NX]
float* divergence_profile, // [NX]
float* stress_xx_profile); // [NX]
```
Host DMA's the four NX-length arrays back, includes in telemetry as compact JSON arrays (every Kth column with significant activity, K chosen so JSON stays under ~4KB per snapshot).
**Telemetry byte budget — confirm before first run.** Naive size at NX=512 is:
| Component | Size |
|-----------|------|
| 4 profile arrays × 512 floats × ~8 chars per JSON float (`-1.234e-2,`) | ~16 KB |
| Global scalars (rho_total, asymmetry, coherence, regime_product, ...) | ~0.5 KB |
| Event detector output (typically 010 events, struct ≈ 80 bytes JSON) | ~1 KB max |
Unsubsampled ≈ 17 KB per snapshot. Even at K=2 (every other column) we are at ~9 KB — well above the 4 KB target. **Mitigations, applied in order until budget met:**
1. Subsample columns with `K=4` (every 4th column) when no event detector has fired in the last N snapshots → ~4 KB.
2. Emit profiles as `float16` hex-encoded (4 chars/value) instead of decimal JSON → halves the dominant cost.
3. Switch profile transport from JSON to ZMQ multipart binary frames (frame 0 = JSON scalars + events, frame 1 = raw float32 binary blob of profiles, 8 KB exactly).
**ZMQ socket configuration.** v5 telemetry is scalar-only (~1 KB/snapshot at 10 Hz = 10 KB/s). v1 telemetry is at least 516× larger. Before first run, the kernel author MUST verify on the publisher (`d_telemetry_pub`):
- `ZMQ_SNDHWM` ≥ 1024 (default in v5 should be checked).
- `ZMQ_SNDBUF` ≥ 256 KB (raise from OS default if needed).
- Snapshot cadence stays at `cycle % 10 == 0` initially — do not raise frequency until profile size is bounded.
If any subscriber (Fractonaut, dashboard) starts dropping or lagging, that is the size budget being exceeded. Fall back to mitigation 3 (binary frames) before tuning anything else.
### C.3 Event detectors (host)
Functions, not kernels \u2014 cheap on NX=512:
```c
typedef struct {
int col; // column index of detected feature
float price; // absolute price at col (translated via d_mid_price + d_tick_size)
float magnitude; // z-score
int persisted_rows;
} structural_event;
int detect_breakouts (const float* divergence_profile,
const float* density_profile,
structural_event* out, int max_out);
int detect_consolidation (const float* vorticity_profile,
const float* divergence_profile,
structural_event* out, int max_out);
int detect_support_levels (const float* density_profile_history, // [NY][NX] ring
structural_event* out, int max_out);
int detect_resistance (const float* density_profile_history, // [NY][NX] ring
structural_event* out, int max_out);
```
Detector thresholds (z-score floors, persistence row counts) start as compile-time defaults and become runtime-tunable in a later pass.
### C.4 last_trade_age_s tracking (host)
```c
static double last_trade_unix = 0.0;
// In telemetry assembly:
double now = ...;
int last_trade_age_s = (int)(now - last_trade_unix);
// Snippet added to JSON: "last_trade_age_s":%d
```
Fractonaut prompt rule (\u00a79 staleness handling): if age > 60, treat as drifting toward equilibrium, structural-only.
### C.5 New ZMQ ports
Change all ZMQ bind ports from 5556\u20135560 to **5566\u20135570** so trade lattice coexists with physics lattice. Host-side: update `endpoint` strings in `main()`. No kernel impact.
---
## Section D \u2014 What is removed (REMOVE)
- `__device__ float d_khra_amp` and `d_gixx_amp` (replaced).
- `khra_gixx_wave_1024` (replaced by `market_wave`).
- `set_khra_amp`, `set_gixx_amp`, `inject_density` command handlers.
- The `asymmetry_factor = 1.0f + sinf(cycle * 0.05f) * 0.5f` envelope: it was a v5 visualisation trick, has no market analogue.
- The `ky = kx * 0.5f` anisotropy in `collide_kernel_khragixx`: rejected per architecture \u00a72.
- The `cy` argument to `inject_density_kernel` (trades always land at y=0).
- The periodic-distance code in inject (`if (dx > NX * 0.5f) dx -= NX;` etc): X is not periodic on price grid.
---
## Section E \u2014 Build and deploy
Build command unchanged from v5 (assumes the existing CMake / nvcc setup discovers the new .cu file automatically). If the build script targets v5 by name:
```diff
- nvcc -O3 -arch=sm_75 khra_gixx_1024_v5.cu -o khra_gixx_1024_v5 \
+ nvcc -O3 -arch=sm_75 trade_lbm_v1.cu -o trade_lbm_v1 \
-lzmq -lnvidia-ml -rdynamic
```
`-arch=sm_75` should match the actual GPU (the v5 build uses this on Beast; confirm at compile time).
Output binary: `/mnt/d/Resonance_Engine/beast-build/trade_lbm_v1` (parallels the existing `khra_gixx_1024_v5` location). Launch with `--agent-owner=RESONANCE` per the BEAST ledger.
---
## Section F \u2014 What is intentionally NOT in v1
- Multi-instrument support. One symbol per binary.
- GPU-side event detection. CPU is enough at NX=512.
- Live trading hooks. Trade lattice output goes to Fractonaut + JSON only.
- Adaptive tick sizing. Constant per startup.
- Auto-tuning of `TAKER_WEIGHT`, `DEPTH_WEIGHT`, `BIAS_WEIGHT`. All compile-time in v1; tuned during validation in \u00a710.
---
## Section G \u2014 Verification checklist for the kernel author
Before declaring trade_lbm_v1 ready for the \u00a710 validation gate:
1. `set_oi_drive value=0.0` + `set_flow_drive value=0.0` + no `set_book` calls (so `d_rho_eq` is zero-initialised) \u2192 lattice should sit at zero everywhere, `regime_product` ~ 0. If non-zero, equilibrium reads aren't wired.
2. `set_book` with uniform bid=ask=1.0 across all columns \u2192 lattice should relax to `regime_product` close to the trade-lattice equivalent of the v5 \"normal\" attractor. Magnitude TBD by calibration but must be *flat* across columns.
3. `set_book` with one heavy column \u2192 corresponding `density_profile[that column]` must build a persistent peak; relaxation rate matches expected 1/\u03c9 timescale for that column's omega.
4. `inject_trade` with price out of range \u2192 `off_grid_mass` increments; nothing else.
5. `set_mid` larger than `RECENTRE_THRESHOLD` away \u2192 `recenter_event` fires; `density_profile` shifts by the expected number of columns; old peaks stay at their absolute price (verified by structural-event detector reporting same prices, different columns).
6. Send a synthetic trade at known price with known size \u2192 verify `density_profile` peak at the correct column with the correct magnitude (`size * TAKER_WEIGHT` if aggressor flag set). **Note (added 2026-06-08 after first-run verification):** Test 6 must run with `omega_profile <= 0.1` across all columns to observe one-shot injection impact. Default omega=1.0 correctly absorbs single trades in one cycle — this is correct BGK behaviour when the book is the attractor, not a bug. Without lowering omega before injecting, the next telemetry snapshot will show the book-derived equilibrium (flat) regardless of the injection.
If all six pass, kernel is ready for replay against April HL data per \u00a710.
---
## Section H \u2014 Estimated kernel-author effort
| Item | Approx LOC delta vs v5 |
|------|------------------------|
| ALTER changes (collide kernel, inject kernel, wave function, telemetry JSON, command handler) | ~250 lines edited |
| ADD: recentre_field | ~40 lines |
| ADD: compute_column_profiles | ~50 lines |
| ADD: 4 event detectors (host) | ~150 lines |
| ADD: recompute_equilibrium_from_book (formula in B.9.1) + book/omega/etc host scratch buffers | ~100 lines (formula is ~40 of these, copy verbatim from B.9.1) |
| ADD: 8 new command-handler branches | ~150 lines |
| REMOVE | ~80 lines |
Net target file size: ~1600 LOC (vs v5's 1284). Well within single-file scope.
The most error-prone item is B.4 (collision kernel using book-derived equilibrium). The most tedious item is C.3 (event detectors). The most subtle item is C.1 (recentre logic, especially making sure the absolute-price reports stay correct after a shift).
---
End of diff-spec. Ready for kernel author review and the answers to Section G verification before any binary is shipped to validation.