diff --git a/Cargo.lock b/Cargo.lock index 74768f2a9..1a6ade1a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5141,6 +5141,48 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "neural-trader-coherence" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "neural-trader-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", +] + +[[package]] +name = "neural-trader-replay" +version = "0.1.0" +dependencies = [ + "anyhow", + "neural-trader-coherence", + "neural-trader-core", + "serde", + "serde_json", +] + +[[package]] +name = "neural-trader-wasm" +version = "0.1.1" +dependencies = [ + "console_error_panic_hook", + "neural-trader-coherence", + "neural-trader-core", + "neural-trader-replay", + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", + "wasm-bindgen-test", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index 7a48ba6e9..8d6825aa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,6 +98,10 @@ members = [ "crates/ruvector-dither", "crates/ruvector-robotics", "examples/robotics", + "crates/neural-trader-core", + "crates/neural-trader-coherence", + "crates/neural-trader-replay", + "crates/neural-trader-wasm", ] resolver = "2" diff --git a/README.md b/README.md index f84cba27a..222704864 100644 --- a/README.md +++ b/README.md @@ -502,7 +502,7 @@ npx @ruvector/rvf-mcp-server --transport stdio # MCP server for AI agents **Rust crates** (23): [`rvf-types`](https://crates.io/crates/rvf-types) `rvf-wire` `rvf-manifest` `rvf-quant` `rvf-index` `rvf-crypto` [`rvf-runtime`](https://crates.io/crates/rvf-runtime) `rvf-kernel` `rvf-ebpf` [`rvf-federation`](./crates/rvf/rvf-federation) `rvf-launch` `rvf-server` `rvf-import` [`rvf-cli`](https://crates.io/crates/rvf-cli) `rvf-wasm` `rvf-solver-wasm` `rvf-node` + 6 adapters (claude-flow, agentdb, ospipe, agentic-flow, rvlite, sona) -**npm packages** (5): [`@ruvector/rvf`](https://www.npmjs.com/package/@ruvector/rvf) [`@ruvector/rvf-node`](https://www.npmjs.com/package/@ruvector/rvf-node) [`@ruvector/rvf-wasm`](https://www.npmjs.com/package/@ruvector/rvf-wasm) [`@ruvector/rvf-mcp-server`](https://www.npmjs.com/package/@ruvector/rvf-mcp-server) [`@ruvector/ruvllm-wasm`](https://www.npmjs.com/package/@ruvector/ruvllm-wasm) +**npm packages** (6): [`@ruvector/rvf`](https://www.npmjs.com/package/@ruvector/rvf) [`@ruvector/rvf-node`](https://www.npmjs.com/package/@ruvector/rvf-node) [`@ruvector/rvf-wasm`](https://www.npmjs.com/package/@ruvector/rvf-wasm) [`@ruvector/rvf-mcp-server`](https://www.npmjs.com/package/@ruvector/rvf-mcp-server) [`@ruvector/ruvllm-wasm`](https://www.npmjs.com/package/@ruvector/ruvllm-wasm) [`@ruvector/neural-trader-wasm`](https://www.npmjs.com/package/@ruvector/neural-trader-wasm) - **Security Hardened RVF** ([`examples/security_hardened.rvf`](./examples/security_hardened.rvf)) β€” 2.1 MB sealed artifact with 22 verified capabilities: TEE attestation (SGX/SEV-SNP/TDX/ARM CCA), AIDefence (injection/jailbreak/PII/exfil), hardened Linux microkernel, eBPF firewall, Ed25519 signing, 6-role RBAC, Coherence Gate, 30-entry witness chain, Paranoid policy, COW branching, audited k-NN. See [ADR-042](./docs/adr/ADR-042-Security-RVF-AIDefence-TEE.md). - **Full documentation**: [crates/rvf/README.md](./crates/rvf/README.md) @@ -1440,7 +1440,7 @@ RuVector runs on Node.js, Rust, browsers, PostgreSQL, and Docker. Pick the packa
-πŸ¦€ Rust Crates (83 Packages) +πŸ¦€ Rust Crates (87 Packages) All crates are published to [crates.io](https://crates.io) under the `ruvector-*` and `rvf-*` namespaces. @@ -1535,6 +1535,17 @@ wget https://huggingface.co/ruv/ruvltra/resolve/main/ruvltra-small-0.5b-q4_k_m.g **Hybrid Routing** achieves **90% accuracy** for agent routing using keyword-first strategy with embedding fallback. See [Issue #122](https://github.com/ruvnet/ruvector/issues/122) for benchmarks and the [training tutorials](#-ruvllm-training--fine-tuning-tutorials) for fine-tuning guides. +### Neural Trader + +| Crate | Description | crates.io | +|-------|-------------|-----------| +| [neural-trader-core](./crates/neural-trader-core) | Market event types, ingest pipeline, graph schema | `publish = false` | +| [neural-trader-coherence](./crates/neural-trader-coherence) | MinCut coherence gate with CUSUM drift detection | `publish = false` | +| [neural-trader-replay](./crates/neural-trader-replay) | Reservoir replay store with proof-gated writes | `publish = false` | +| [neural-trader-wasm](./crates/neural-trader-wasm) | WASM bindings for browser coherence gates & replay | `publish = false` | + +Six-layer pipeline treating the limit order book as a dynamic heterogeneous typed graph. See [ADR-085](./docs/adr/ADR-085-neural-trader-ruvector.md) for architecture, [ADR-086](./docs/adr/ADR-086-neural-trader-wasm.md) for WASM bindings. + ### Dynamic Min-Cut (December 2025 Breakthrough) | Crate | Description | crates.io | diff --git a/crates/mcp-brain-server/static/index.html b/crates/mcp-brain-server/static/index.html index ebcace0a7..3e6802f4d 100644 --- a/crates/mcp-brain-server/static/index.html +++ b/crates/mcp-brain-server/static/index.html @@ -1286,6 +1286,68 @@

Access the brain

} scene.add(ringGroup); +// ── Animated squares (billboard sprites that always face camera) ── +function createSquareTexture(size, color) { + const c = document.createElement('canvas'); + c.width = size; c.height = size; + const ctx = c.getContext('2d'); + ctx.clearRect(0, 0, size, size); + const pad = size * 0.15; + ctx.strokeStyle = color; + ctx.lineWidth = size * 0.04; + ctx.globalAlpha = 0.9; + ctx.strokeRect(pad, pad, size - pad * 2, size - pad * 2); + ctx.globalAlpha = 0.15; + ctx.fillStyle = color; + ctx.fillRect(pad, pad, size - pad * 2, size - pad * 2); + const tex = new THREE.CanvasTexture(c); + tex.needsUpdate = true; + return tex; +} + +const sqTexWarm = createSquareTexture(64, '#e8a634'); +const sqTexBlue = createSquareTexture(64, '#5b8def'); + +const SQUARE_COUNT = isMobile ? 20 : 45; +const squareGroup = new THREE.Group(); +const squareData = []; + +for (let i = 0; i < SQUARE_COUNT; i++) { + const isBlue = Math.random() > 0.75; + const baseOp = 0.12 + Math.random() * 0.18; + const mat = new THREE.SpriteMaterial({ + map: isBlue ? sqTexBlue : sqTexWarm, + transparent: true, + opacity: baseOp, + blending: THREE.AdditiveBlending, + depthWrite: false, + rotation: Math.random() * Math.PI, + }); + const sprite = new THREE.Sprite(mat); + const scale = 0.6 + Math.random() * 2.0; + sprite.scale.set(scale, scale, 1); + + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2 * Math.random() - 1); + const r = 6 + Math.random() * 18; + sprite.position.set( + r * Math.sin(phi) * Math.cos(theta), + r * Math.sin(phi) * Math.sin(theta) * 0.6, + r * Math.cos(phi) + ); + + squareGroup.add(sprite); + squareData.push({ + sprite, + baseY: sprite.position.y, + rotSpeed: (Math.random() - 0.5) * 0.3, + floatSpeed: 0.15 + Math.random() * 0.5, + phase: Math.random() * Math.PI * 2, + baseOpacity: baseOp, + }); +} +scene.add(squareGroup); + // ── Data particles (memories as tiny points of light) ── let particleGroup = null; let particleVelocities = []; @@ -1330,6 +1392,51 @@

Access the brain

} buildParticles(null); +// ── Dot-matrix grid (mouse-reactive anti-gravity wave field) ── +const GRID = isMobile ? 28 : 48; +const GRID_SPACING = isMobile ? 1.8 : 1.4; +const GRID_HALF = (GRID - 1) * GRID_SPACING * 0.5; +const dotCount = GRID * GRID; +const dotPos = new Float32Array(dotCount * 3); +const dotCol = new Float32Array(dotCount * 3); +const dotBase = []; // store rest positions + +for (let ix = 0; ix < GRID; ix++) { + for (let iz = 0; iz < GRID; iz++) { + const idx = ix * GRID + iz; + const x = ix * GRID_SPACING - GRID_HALF; + const z = iz * GRID_SPACING - GRID_HALF; + dotPos[idx * 3] = x; + dotPos[idx * 3 + 1] = -14; // flat plane below center + dotPos[idx * 3 + 2] = z; + dotBase.push({ x, z }); + // Faint warm tint, cooler toward edges + const edgeDist = Math.sqrt(x * x + z * z) / GRID_HALF; + const c = warm.clone().lerp(blue, edgeDist * 0.6).multiplyScalar(0.5 + (1 - edgeDist) * 0.5); + dotCol[idx * 3] = c.r; + dotCol[idx * 3 + 1] = c.g; + dotCol[idx * 3 + 2] = c.b; + } +} + +const dotGeo = new THREE.BufferGeometry(); +dotGeo.setAttribute('position', new THREE.BufferAttribute(dotPos, 3)); +dotGeo.setAttribute('color', new THREE.BufferAttribute(dotCol, 3)); +const dotMat = new THREE.PointsMaterial({ + size: isMobile ? 0.35 : 0.25, + vertexColors: true, + transparent: true, + opacity: 0.18, + blending: THREE.AdditiveBlending, + sizeAttenuation: true, + depthWrite: false, +}); +const dotMatrix = new THREE.Points(dotGeo, dotMat); +scene.add(dotMatrix); + +// Mouse position projected onto the grid plane (world coords) +let mouseWorldX = 0, mouseWorldZ = 0; + // ── Central core (tiny glowing sphere) ── const coreGeo = new THREE.SphereGeometry(0.4, 32, 32); const coreMat = new THREE.MeshBasicMaterial({ color: warm, transparent: true, opacity: 0.6 }); @@ -1367,6 +1474,47 @@

Access the brain

coreMat.opacity = pulse + 0.2; halo.scale.setScalar(1 + Math.sin(time * 2) * 0.2); + // Squares drift + rotate + for (const s of squareData) { + s.sprite.material.rotation += s.rotSpeed * 0.004; + s.sprite.position.y = s.baseY + Math.sin(time * s.floatSpeed + s.phase) * 1.2; + s.sprite.material.opacity = s.baseOpacity * (0.7 + Math.sin(time * 1.2 + s.phase) * 0.3); + } + squareGroup.rotation.y = time * 0.06; + + // Dot-matrix wave field + mouseWorldX += (mouseX * GRID_HALF * 0.8 - mouseWorldX) * 0.08; + mouseWorldZ += (mouseY * GRID_HALF * 0.6 - mouseWorldZ) * 0.08; + const dp = dotMatrix.geometry.attributes.position.array; + for (let i = 0; i < dotCount; i++) { + const bx = dotBase[i].x; + const bz = dotBase[i].z; + const dx = bx - mouseWorldX; + const dz = bz - mouseWorldZ; + const dist = Math.sqrt(dx * dx + dz * dz); + // Anti-gravity repulsion dome around cursor + const repulse = Math.max(0, 1 - dist / (GRID_HALF * 0.45)); + const lift = repulse * repulse * 12; + // Hyperdimensional ripple waves β€” deep multi-frequency + const wave1 = Math.sin(dist * 0.4 - time * 6) * 1.0 * repulse; + const wave2 = Math.sin(bx * 0.3 + bz * 0.3 + time * 2) * 0.4; + const wave3 = Math.cos(dist * 0.2 + time * 3) * 0.5 * (1 - repulse * 0.5); + // Breathing ambient undulation across entire grid + const breath = Math.sin(bx * 0.12 + time * 1.5) * Math.cos(bz * 0.12 + time * 1.2) * 0.8; + // Concentric pulse rings expanding from center + const centerDist = Math.sqrt(bx * bx + bz * bz); + const pulse2 = Math.sin(centerDist * 0.25 - time * 4) * 0.5; + // High-frequency dimensional shimmer + const shimmer = Math.sin(bx * 0.8 - bz * 0.6 + time * 8) * 0.12 * repulse; + dp[i * 3 + 1] = -14 + lift + wave1 + wave2 + wave3 + breath + pulse2 + shimmer; + // Deeper xy displacement β€” swirling vortex near cursor + const angle = Math.atan2(dz, dx); + const spiral = repulse * repulse * 1.2; + dp[i * 3] = bx + Math.sin(angle + time * 3) * spiral + Math.sin(dist * 0.3 - time * 4) * repulse * 0.6; + dp[i * 3 + 2] = bz + Math.cos(angle + time * 3) * spiral + Math.cos(dist * 0.3 - time * 4) * repulse * 0.6; + } + dotMatrix.geometry.attributes.position.needsUpdate = true; + // Particles drift if (particleGroup) { const pp = particleGroup.geometry.attributes.position.array; @@ -1411,7 +1559,8 @@

Access the brain

const status = statusResp.ok ? await statusResp.json() : null; const health = healthResp.ok ? await healthResp.json() : null; - const memories = memResp.ok ? await memResp.json() : []; + const memJson = memResp.ok ? await memResp.json() : []; + const memories = Array.isArray(memJson) ? memJson : (Array.isArray(memJson.memories) ? memJson.memories : []); if (status) { console.log('[Ο€] Status data:', JSON.stringify(status)); diff --git a/crates/mcp-brain-server/static/origin.html b/crates/mcp-brain-server/static/origin.html index 4fa0fc5ef..b45a55361 100644 --- a/crates/mcp-brain-server/static/origin.html +++ b/crates/mcp-brain-server/static/origin.html @@ -297,14 +297,93 @@ return t; } +// ── Persistent dot-matrix field (survives scene transitions) ── +const GRID = isMobile ? 24 : 40; +const GRID_SP = isMobile ? 2.0 : 1.5; +const GRID_HALF = (GRID - 1) * GRID_SP * 0.5; +const dotCount = GRID * GRID; +const dotPos = new Float32Array(dotCount * 3); +const dotCol = new Float32Array(dotCount * 3); +const dotBase = []; + +for (let ix = 0; ix < GRID; ix++) { + for (let iz = 0; iz < GRID; iz++) { + const idx = ix * GRID + iz; + const x = ix * GRID_SP - GRID_HALF; + const z = iz * GRID_SP - GRID_HALF; + dotPos[idx * 3] = x; + dotPos[idx * 3 + 1] = -12; + dotPos[idx * 3 + 2] = z; + dotBase.push({ x, z }); + const edgeDist = Math.sqrt(x * x + z * z) / GRID_HALF; + const c = warm.clone().lerp(blue, edgeDist * 0.5).multiplyScalar(0.5 + (1 - edgeDist) * 0.5); + dotCol[idx * 3] = c.r; dotCol[idx * 3 + 1] = c.g; dotCol[idx * 3 + 2] = c.b; + } +} +const dotGeo = new THREE.BufferGeometry(); +dotGeo.setAttribute('position', new THREE.BufferAttribute(dotPos, 3)); +dotGeo.setAttribute('color', new THREE.BufferAttribute(dotCol, 3)); +const dotMat = new THREE.PointsMaterial({ + size: isMobile ? 0.3 : 0.22, vertexColors: true, transparent: true, + opacity: 0.15, blending: THREE.AdditiveBlending, sizeAttenuation: true, depthWrite: false, +}); +const dotMatrix = new THREE.Points(dotGeo, dotMat); +threeScene.add(dotMatrix); + +let mouseWorldX = 0, mouseWorldZ = 0; +let matrixIntensity = 0.5; // varies per scene +let matrixMode = 'calm'; // calm, surge, split, spiral, shield, radiant + +// Scene-specific matrix behaviors +const matrixModes = { + noise: { mode: 'calm', intensity: 0.3, opacity: 0.10 }, + singularity: { mode: 'surge', intensity: 0.6, opacity: 0.15 }, + birth: { mode: 'radiant', intensity: 0.8, opacity: 0.20 }, + containers: { mode: 'grid', intensity: 0.5, opacity: 0.15 }, + cut: { mode: 'split', intensity: 0.7, opacity: 0.18 }, + graph: { mode: 'spiral', intensity: 0.6, opacity: 0.16 }, + attention: { mode: 'surge', intensity: 0.9, opacity: 0.22 }, + shield: { mode: 'shield', intensity: 0.7, opacity: 0.18 }, + transfer: { mode: 'split', intensity: 0.6, opacity: 0.16 }, + collective: { mode: 'spiral', intensity: 0.8, opacity: 0.20 }, + exo: { mode: 'surge', intensity: 0.7, opacity: 0.18 }, + radiant: { mode: 'radiant', intensity: 1.0, opacity: 0.25 }, +}; + +let targetOpacity = 0.15; +let transitionWave = 0; // spike on scene change + +function setMatrixForScene(vizType) { + const cfg = matrixModes[vizType] || matrixModes.noise; + matrixMode = cfg.mode; + matrixIntensity = cfg.intensity; + targetOpacity = cfg.opacity; + transitionWave = 1.0; // trigger a transition burst +} + // ── Build visualization per scene type ── function clearViz() { - while (threeScene.children.length) threeScene.remove(threeScene.children[0]); + // Remove everything EXCEPT the dot matrix + const keep = [dotMatrix]; + while (threeScene.children.length) { + const child = threeScene.children[0]; + if (keep.includes(child)) { + if (threeScene.children.length === keep.length) break; + // Move to end to process others + threeScene.remove(child); + threeScene.add(child); + continue; + } + threeScene.remove(child); + } + // Re-add matrix if it was removed + if (!threeScene.children.includes(dotMatrix)) threeScene.add(dotMatrix); particles = null; sprites = []; particleVels = []; } function buildViz(type) { clearViz(); + setMatrixForScene(type); const n = isMobile ? 80 : 200; pCount = n; @@ -516,6 +595,78 @@ s.sp.material.rotation = Math.sin(time * 0.2 + s.phase) * 0.1; } + // ── Dot-matrix wave field (persistent, varies per scene) ── + mouseWorldX += (mouseX * GRID_HALF * 0.7 - mouseWorldX) * 0.06; + mouseWorldZ += (mouseY * GRID_HALF * 0.5 - mouseWorldZ) * 0.06; + // Smooth opacity transitions + dotMat.opacity += (targetOpacity - dotMat.opacity) * 0.04; + // Decay transition wave + transitionWave *= 0.97; + + const dp = dotMatrix.geometry.attributes.position.array; + const mi = matrixIntensity; + for (let i = 0; i < dotCount; i++) { + const bx = dotBase[i].x; + const bz = dotBase[i].z; + const dx = bx - mouseWorldX; + const dz = bz - mouseWorldZ; + const dist = Math.sqrt(dx * dx + dz * dz); + const centerDist = Math.sqrt(bx * bx + bz * bz); + + // Mouse anti-gravity dome + const repulse = Math.max(0, 1 - dist / (GRID_HALF * 0.4)); + const lift = repulse * repulse * 10 * mi; + + // Transition shockwave β€” expanding ring on scene change + const twR = (1 - transitionWave) * GRID_HALF * 1.5; + const twDist = Math.abs(centerDist - twR); + const twLift = transitionWave * Math.max(0, 1 - twDist / 8) * 6; + + let y = -12; + if (matrixMode === 'calm') { + y += Math.sin(bx * 0.15 + time * 1.5) * Math.cos(bz * 0.15 + time * 1.2) * 0.5 * mi; + y += Math.sin(centerDist * 0.2 - time * 2) * 0.3 * mi; + } else if (matrixMode === 'surge') { + y += Math.sin(centerDist * 0.25 - time * 5) * 1.0 * mi; + y += Math.sin(bx * 0.2 + time * 2.5) * Math.cos(bz * 0.2 + time * 2) * 0.6 * mi; + y += Math.sin(dist * 0.3 - time * 6) * 0.8 * repulse * mi; + } else if (matrixMode === 'split') { + const side = bx > 0 ? 1 : -1; + y += Math.sin(Math.abs(bx) * 0.3 - time * 3) * 0.8 * mi; + y += side * Math.sin(bz * 0.2 + time * 2) * 0.5 * mi; + y += Math.cos(bx * 0.15 + bz * 0.15 + time * 4) * 0.3 * mi; + } else if (matrixMode === 'spiral') { + const angle = Math.atan2(bz, bx); + y += Math.sin(angle * 3 + centerDist * 0.2 - time * 4) * 0.8 * mi; + y += Math.sin(centerDist * 0.15 - time * 3) * 0.6 * mi; + } else if (matrixMode === 'shield') { + for (let r = 1; r <= 7; r++) { + const ringR = 3 + r * 2.5; + const ringDist = Math.abs(centerDist - ringR * 0.6); + y += Math.max(0, 1 - ringDist / 2.5) * Math.sin(time * 3 + r) * 0.5 * mi; + } + } else if (matrixMode === 'grid') { + y += Math.sin(bx * 0.5) * Math.sin(bz * 0.5) * Math.sin(time * 2) * 0.6 * mi; + y += Math.cos(bx * 0.3 + bz * 0.3 + time * 3) * 0.3 * mi; + } else if (matrixMode === 'radiant') { + y += Math.sin(centerDist * 0.3 - time * 5) * 1.2 * mi; + y += Math.sin(bx * 0.2 + time * 3) * Math.cos(bz * 0.2 + time * 2.5) * 0.8 * mi; + y += Math.cos(dist * 0.25 - time * 4) * 0.6 * repulse * mi; + const angle = Math.atan2(bz, bx); + y += Math.sin(angle * 4 + time * 2) * 0.3 * mi; + } + + y += lift + twLift; + dp[i * 3 + 1] = y; + + // XZ displacement β€” vortex near cursor + const angle2 = Math.atan2(dz, dx); + const spiral2 = repulse * repulse * 1.0 * mi; + dp[i * 3] = bx + Math.sin(angle2 + time * 3) * spiral2; + dp[i * 3 + 2] = bz + Math.cos(angle2 + time * 3) * spiral2; + } + dotMatrix.geometry.attributes.position.needsUpdate = true; + // Camera with mouse parallax camera.position.x = Math.sin(time * 0.1) * 2 + mouseX * 5; camera.position.y = Math.cos(time * 0.08) * 1 + mouseY * 3; diff --git a/crates/neural-trader-coherence/Cargo.toml b/crates/neural-trader-coherence/Cargo.toml new file mode 100644 index 000000000..68d4d223e --- /dev/null +++ b/crates/neural-trader-coherence/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "neural-trader-coherence" +version = "0.1.0" +edition = "2021" +description = "MinCut coherence gate, CUSUM drift detection, and proof-gated mutation for Neural Trader" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } + +[dev-dependencies] diff --git a/crates/neural-trader-coherence/src/lib.rs b/crates/neural-trader-coherence/src/lib.rs new file mode 100644 index 000000000..45da417aa --- /dev/null +++ b/crates/neural-trader-coherence/src/lib.rs @@ -0,0 +1,301 @@ +//! # neural-trader-coherence +//! +//! MinCut coherence gate, CUSUM drift detection, and proof-gated +//! mutation protocol for the RuVector Neural Trader (ADR-084). + +use serde::{Deserialize, Serialize}; + +// --------------------------------------------------------------------------- +// Coherence decision +// --------------------------------------------------------------------------- + +/// Result of the coherence gate evaluation. +/// +/// Every memory write, model update, retrieval, and actuation must pass +/// through this gate before proceeding. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CoherenceDecision { + pub allow_retrieve: bool, + pub allow_write: bool, + pub allow_learn: bool, + pub allow_act: bool, + pub mincut_value: u64, + pub partition_hash: [u8; 16], + pub drift_score: f32, + pub cusum_score: f32, + pub reasons: Vec, +} + +impl CoherenceDecision { + /// Returns `true` if all gates are open. + pub fn all_allowed(&self) -> bool { + self.allow_retrieve && self.allow_write && self.allow_learn && self.allow_act + } + + /// Returns `true` if nothing is allowed. + pub fn fully_blocked(&self) -> bool { + !self.allow_retrieve && !self.allow_write && !self.allow_learn && !self.allow_act + } +} + +// --------------------------------------------------------------------------- +// Gate context +// --------------------------------------------------------------------------- + +/// Input context for the coherence gate evaluation. +#[derive(Debug, Clone)] +pub struct GateContext { + pub symbol_id: u32, + pub venue_id: u16, + pub ts_ns: u64, + /// Current mincut value from the local induced subgraph. + pub mincut_value: u64, + /// Partition hash over boundary nodes. + pub partition_hash: [u8; 16], + /// Rolling CUSUM score for drift detection. + pub cusum_score: f32, + /// Embedding drift magnitude since last stable window. + pub drift_score: f32, + /// Current regime label. + pub regime: RegimeLabel, + /// Number of consecutive windows with stable boundary identity. + /// Gate requires this >= `GateConfig::boundary_stability_windows`. + pub boundary_stable_count: usize, +} + +/// Coarse regime classification. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum RegimeLabel { + Calm, + Normal, + Volatile, +} + +// --------------------------------------------------------------------------- +// Gate configuration +// --------------------------------------------------------------------------- + +/// Configurable thresholds for the coherence gate. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GateConfig { + pub mincut_floor_calm: u64, + pub mincut_floor_normal: u64, + pub mincut_floor_volatile: u64, + pub cusum_threshold: f32, + pub boundary_stability_windows: usize, + pub max_drift_score: f32, +} + +impl Default for GateConfig { + fn default() -> Self { + Self { + mincut_floor_calm: 12, + mincut_floor_normal: 9, + mincut_floor_volatile: 6, + cusum_threshold: 4.5, + boundary_stability_windows: 8, + max_drift_score: 0.5, + } + } +} + +// --------------------------------------------------------------------------- +// Coherence gate trait and default implementation +// --------------------------------------------------------------------------- + +/// Evaluates coherence of the current market-graph state. +pub trait CoherenceGate { + fn evaluate(&self, ctx: &GateContext) -> anyhow::Result; +} + +/// Default threshold-based coherence gate. +pub struct ThresholdGate { + pub config: GateConfig, +} + +impl ThresholdGate { + pub fn new(config: GateConfig) -> Self { + Self { config } + } +} + +impl CoherenceGate for ThresholdGate { + fn evaluate(&self, ctx: &GateContext) -> anyhow::Result { + let floor = match ctx.regime { + RegimeLabel::Calm => self.config.mincut_floor_calm, + RegimeLabel::Normal => self.config.mincut_floor_normal, + RegimeLabel::Volatile => self.config.mincut_floor_volatile, + }; + + let cut_ok = ctx.mincut_value >= floor; + let cusum_ok = ctx.cusum_score < self.config.cusum_threshold; + let drift_ok = ctx.drift_score < self.config.max_drift_score; + let boundary_ok = + ctx.boundary_stable_count >= self.config.boundary_stability_windows; + // Learning requires tighter drift margin (half the max). + let learn_drift_ok = ctx.drift_score < self.config.max_drift_score * 0.5; + + let mut reasons = Vec::new(); + if !cut_ok { + reasons.push(format!( + "mincut {} below floor {} for {:?}", + ctx.mincut_value, floor, ctx.regime + )); + } + if !cusum_ok { + reasons.push(format!( + "CUSUM {:.3} exceeds threshold {:.3}", + ctx.cusum_score, self.config.cusum_threshold + )); + } + if !drift_ok { + reasons.push(format!( + "drift {:.3} exceeds max {:.3}", + ctx.drift_score, self.config.max_drift_score + )); + } + if !boundary_ok { + reasons.push(format!( + "boundary stable for {} windows, need {}", + ctx.boundary_stable_count, self.config.boundary_stability_windows + )); + } + + let base_ok = cut_ok && cusum_ok && drift_ok && boundary_ok; + + Ok(CoherenceDecision { + allow_retrieve: cut_ok, // retrieval is most permissive + allow_write: base_ok, + allow_learn: base_ok && learn_drift_ok, // stricter: half drift margin + allow_act: base_ok, + mincut_value: ctx.mincut_value, + partition_hash: ctx.partition_hash, + drift_score: ctx.drift_score, + cusum_score: ctx.cusum_score, + reasons, + }) + } +} + +// --------------------------------------------------------------------------- +// Proof-gated mutation +// --------------------------------------------------------------------------- + +/// A verified mutation token. Only issued when the coherence gate and +/// policy kernel both approve. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VerifiedToken { + pub token_id: [u8; 16], + pub ts_ns: u64, + pub coherence_hash: [u8; 16], + pub policy_hash: [u8; 16], + pub action_intent: String, +} + +/// Witness receipt appended after every state mutation. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WitnessReceipt { + pub ts_ns: u64, + pub model_id: String, + pub input_segment_hash: [u8; 16], + pub coherence_witness_hash: [u8; 16], + pub policy_hash: [u8; 16], + pub action_intent: String, + pub verified_token_id: [u8; 16], + pub resulting_state_hash: [u8; 16], +} + +/// Logs witness receipts for auditability. +pub trait WitnessLogger { + fn append_receipt(&mut self, receipt: WitnessReceipt) -> anyhow::Result<()>; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + fn make_ctx(mincut: u64, cusum: f32, drift: f32, regime: RegimeLabel) -> GateContext { + GateContext { + symbol_id: 1, + venue_id: 1, + ts_ns: 0, + mincut_value: mincut, + partition_hash: [0u8; 16], + cusum_score: cusum, + drift_score: drift, + regime, + boundary_stable_count: 10, // above default threshold of 8 + } + } + + #[test] + fn calm_regime_passes_when_above_floor() { + let gate = ThresholdGate::new(GateConfig::default()); + let ctx = make_ctx(15, 1.0, 0.1, RegimeLabel::Calm); + let d = gate.evaluate(&ctx).unwrap(); + assert!(d.all_allowed()); + assert!(d.reasons.is_empty()); + } + + #[test] + fn calm_regime_blocks_when_below_floor() { + let gate = ThresholdGate::new(GateConfig::default()); + let ctx = make_ctx(5, 1.0, 0.1, RegimeLabel::Calm); + let d = gate.evaluate(&ctx).unwrap(); + assert!(!d.allow_act); + assert!(!d.allow_write); + } + + #[test] + fn cusum_breach_blocks_learning() { + let gate = ThresholdGate::new(GateConfig::default()); + let ctx = make_ctx(15, 5.0, 0.1, RegimeLabel::Calm); + let d = gate.evaluate(&ctx).unwrap(); + assert!(!d.allow_learn); + } + + #[test] + fn volatile_regime_has_lower_floor() { + let gate = ThresholdGate::new(GateConfig::default()); + let ctx = make_ctx(7, 1.0, 0.1, RegimeLabel::Volatile); + let d = gate.evaluate(&ctx).unwrap(); + assert!(d.all_allowed()); + } + + #[test] + fn drift_blocks_learning() { + let gate = ThresholdGate::new(GateConfig::default()); + // drift 0.4 > max_drift*0.5 (0.25) so learning blocked, + // but < max_drift (0.5) so act/write still allowed. + let ctx = make_ctx(15, 1.0, 0.4, RegimeLabel::Calm); + let d = gate.evaluate(&ctx).unwrap(); + assert!(!d.allow_learn); + assert!(d.allow_act); // act is still permitted + } + + #[test] + fn high_drift_blocks_everything() { + let gate = ThresholdGate::new(GateConfig::default()); + let ctx = make_ctx(15, 1.0, 0.8, RegimeLabel::Calm); + let d = gate.evaluate(&ctx).unwrap(); + assert!(!d.allow_learn); + assert!(!d.allow_act); + assert!(!d.allow_write); + assert!(d.allow_retrieve); // retrieval only needs cut_ok + } + + #[test] + fn boundary_instability_blocks_action() { + let gate = ThresholdGate::new(GateConfig::default()); + let mut ctx = make_ctx(15, 1.0, 0.1, RegimeLabel::Calm); + ctx.boundary_stable_count = 3; // below threshold of 8 + let d = gate.evaluate(&ctx).unwrap(); + assert!(!d.allow_act); + assert!(!d.allow_write); + assert!(d.allow_retrieve); // retrieval permissive + } +} diff --git a/crates/neural-trader-core/Cargo.toml b/crates/neural-trader-core/Cargo.toml new file mode 100644 index 000000000..105756206 --- /dev/null +++ b/crates/neural-trader-core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "neural-trader-core" +version = "0.1.0" +edition = "2021" +description = "Canonical market event types, ingest pipeline, and graph schema for RuVector Neural Trader" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } + +[dev-dependencies] +serde_json = "1" diff --git a/crates/neural-trader-core/README.md b/crates/neural-trader-core/README.md new file mode 100644 index 000000000..fd6a4bf85 --- /dev/null +++ b/crates/neural-trader-core/README.md @@ -0,0 +1,335 @@ +# Neural Trader β€” Coherence-Gated Market Intelligence + +A RuVector-native market intelligence stack that treats the limit order book as a **dynamic heterogeneous typed graph** with proof-gated mutation, MinCut coherence gating, and witnessable replay memory. + +> **Do not trust prediction alone. Trust prediction only when the surrounding market structure is coherent enough to justify learning, remembering, or acting.** + +## Architecture + +``` +L1 Ingest β†’ L2 Graph β†’ L3 GNN+Attention β†’ L4 Memory β†’ L5 Coherence β†’ L6 Policy +``` + +| Layer | Crate | Status | Description | +|-------|-------|--------|-------------| +| L1 | [`neural-trader-core`](../neural-trader-core) | Implemented | Canonical event types, graph schema, ingest traits | +| L2 | `neural-trader-graph` | Planned | Dynamic heterogeneous typed graph construction | +| L3 | `neural-trader-gnn` | Planned | GNN embeddings + multi-head temporal attention | +| L4 | [`neural-trader-replay`](../neural-trader-replay) | Implemented | Reservoir store with gated writes + witness receipts | +| L5 | [`neural-trader-coherence`](../neural-trader-coherence) | Implemented | MinCut coherence gate, CUSUM drift detection | +| L6 | `neural-trader-policy` | Planned | Policy actuation with position sizing | +| WASM | [`neural-trader-wasm`](../neural-trader-wasm) | Implemented | Browser bindings via wasm-pack (npm: `@ruvector/neural-trader-wasm`) | + +**ADRs:** [ADR-085](../../docs/adr/ADR-085-neural-trader-ruvector.md) (architecture) | [ADR-086](../../docs/adr/ADR-086-neural-trader-wasm.md) (WASM bindings) + +## Crates + +### neural-trader-core + +Canonical market event types, graph schema, and ingest traits. + +**Types:** + +| Type | Fields | Purpose | +|------|--------|---------| +| `MarketEvent` | event_id, timestamps, venue, symbol, type, side, price, qty, flags, seq | Normalized event envelope for all market data | +| `EventType` | NewOrder, ModifyOrder, CancelOrder, Trade, BookSnapshot, SessionMarker, VenueStatus | 7 event discriminants | +| `Side` | Bid, Ask | Order side | +| `NodeKind` | Symbol, Venue, PriceLevel, Order, Trade, Event, Participant, TimeBucket, Regime, StrategyState | 10 graph node types | +| `EdgeKind` | AtLevel, NextTick, Generated, Matched, ModifiedFrom, CanceledBy, BelongsToSymbol, OnVenue, InWindow, CorrelatedWith, InRegime, AffectsState | 12 edge types | +| `PropertyKey` | VisibleDepth, EstimatedHiddenDepth, QueueLength, LocalImbalance, RefillRate, DepletionRate, SpreadDistance, LocalRealizedVol, CancelHazard, FillHazard, SlippageToMid, PostTradeImpact, InfluenceScore, CoherenceContribution, QueueEstimate, Age, ModifyCount | 17 property keys | +| `GraphDelta` | nodes_added, edges_added, properties_updated | Diff produced by graph projection | +| `StateWindow` | symbol, venue, time range, events | Sliding window for embedding | + +**Traits:** + +```rust +pub trait EventIngestor { + fn ingest(&mut self, event: MarketEvent) -> anyhow::Result<()>; +} + +pub trait GraphUpdater { + fn apply_event(&mut self, event: &MarketEvent) -> anyhow::Result; +} + +pub trait Embedder { + fn embed_state(&self, ctx: &StateWindow) -> anyhow::Result>; +} +``` + +### neural-trader-coherence + +MinCut coherence gate with CUSUM drift detection and proof-gated mutation protocol. + +**Coherence Gate** β€” every memory write, model update, retrieval, and actuation must pass through the gate: + +| Permission | Condition | Use Case | +|------------|-----------|----------| +| `allow_retrieve` | mincut above floor | Most permissive β€” read-only access | +| `allow_write` | mincut + CUSUM + drift + boundary stable | Memory writes | +| `allow_learn` | all of above + drift < 50% of max | Online learning (stricter drift margin) | +| `allow_act` | all base conditions | Live order placement | + +**Regime-Adaptive Thresholds:** + +| Regime | MinCut Floor (default) | Behavior | +|--------|----------------------|----------| +| Calm | 12 | Highest bar β€” full confidence required | +| Normal | 9 | Standard operation | +| Volatile | 6 | Relaxed floor β€” accepts structural uncertainty | + +**CUSUM Drift Detection** monitors parameter drift and blocks mutations when score exceeds threshold (default 4.5). + +**Types:** + +| Type | Purpose | +|------|---------| +| `GateConfig` | All configurable thresholds (mincut floors, CUSUM, drift, boundary windows) | +| `ThresholdGate` | Default gate implementation using regime-adaptive thresholds | +| `GateContext` | Input: symbol, venue, timestamp, mincut, partition hash, scores, regime | +| `CoherenceDecision` | Output: four booleans + diagnostics (cut value, drift, CUSUM, reasons) | +| `RegimeLabel` | Calm, Normal, Volatile | +| `VerifiedToken` | Proof token minted when coherence + policy both approve a mutation | +| `WitnessReceipt` | Immutable audit record appended after every state mutation | + +**Proof-Gated Mutation Protocol:** + +``` +Compute features β†’ Coherence gate β†’ Policy kernel β†’ Mint token β†’ Apply mutation β†’ Append receipt +``` + +### neural-trader-replay + +Witnessable replay segments, reservoir memory store, and audit receipt logging. + +**Replay Segments** β€” sealed, signed windows containing: + +- Compact subgraph events +- Embedding snapshots +- Realized labels (mid-price move, fill outcome) +- Coherence statistics at write time +- Lineage metadata (model ID, policy version) +- Witness hash for tamper detection + +**Segment Classification** (7 kinds): + +| Kind | Trigger | +|------|---------| +| HighUncertainty | Model confidence below threshold | +| LargeImpact | Significant realized PnL impact | +| RegimeTransition | Regime label changed | +| StructuralAnomaly | Graph structure deviated from norm | +| RareQueuePattern | Unusual queue behavior detected | +| HeadDisagreement | Prediction heads disagree | +| Routine | Standard periodic capture | + +**ReservoirStore** β€” bounded memory with O(1) eviction: + +```rust +pub trait MemoryStore { + fn retrieve(&self, query: &MemoryQuery) -> anyhow::Result>; + fn maybe_write(&mut self, seg: ReplaySegment, gate: &CoherenceDecision) -> anyhow::Result; +} +``` + +- Writes are gated by `CoherenceDecision.allow_write` +- VecDeque-backed for O(1) front eviction when full +- Filterable by symbol and regime + +**InMemoryReceiptLog** β€” append-only witness logger for testing and research. + +### neural-trader-wasm + +Browser WASM bindings wrapping all 3 crates. Published as [`@ruvector/neural-trader-wasm`](https://www.npmjs.com/package/@ruvector/neural-trader-wasm) on npm. + +**Exported Classes:** + +| Class | Key Methods | +|-------|-------------| +| `MarketEventWasm` | `new(eventType, symbolId, venueId, priceFp, qtyFp)`, getters/setters, `toJson()`, `fromJson()` | +| `GraphDeltaWasm` | `new()`, `nodesAdded()`, `edgesAdded()`, `propertiesUpdated()` | +| `GateConfigWasm` | `new()` with defaults, all threshold getters/setters | +| `GateContextWasm` | `new(9 params)`, all field getters | +| `ThresholdGateWasm` | `new(config)`, `evaluate(ctx)` | +| `CoherenceDecisionWasm` | `allowRetrieve/Write/Learn/Act`, `partitionHash`, `reasons()`, `toJson()` | +| `ReplaySegmentWasm` | `toJson()`, `fromJson()`, field getters | +| `ReservoirStoreWasm` | `new(maxSize)`, `len()`, `isEmpty()`, `maybeWrite()`, `retrieveBySymbol()` | + +**Exported Enums:** `EventTypeWasm` (7), `SideWasm` (2), `RegimeLabelWasm` (3), `SegmentKindWasm` (7), `NodeKindWasm` (10), `EdgeKindWasm` (12), `PropertyKeyWasm` (17) + +**Features:** +- BigInt-safe serialization (no u64 precision loss on nanosecond timestamps) +- Non-ASCII-safe hex parsing with optional `0x` prefix support +- Zero-size store guard +- 172 KB WASM binary (uncompressed) + +## Quick Start + +### Rust + +```rust +use neural_trader_coherence::{GateConfig, GateContext, ThresholdGate, CoherenceGate, RegimeLabel}; + +// Create a coherence gate with default thresholds +let gate = ThresholdGate::new(GateConfig::default()); + +// Build context from current market state +let ctx = GateContext { + symbol_id: 42, + venue_id: 1, + ts_ns: 1_704_067_200_000_000_000, + mincut_value: 15, + partition_hash: [0u8; 16], + cusum_score: 1.0, + drift_score: 0.1, + regime: RegimeLabel::Calm, + boundary_stable_count: 10, +}; + +let decision = gate.evaluate(&ctx).unwrap(); +assert!(decision.all_allowed()); +``` + +### JavaScript (WASM) + +```js +import init, { + GateConfigWasm, ThresholdGateWasm, GateContextWasm, + RegimeLabelWasm, MarketEventWasm, EventTypeWasm, + ReservoirStoreWasm, version, healthCheck, +} from '@ruvector/neural-trader-wasm'; + +await init(); +console.log(version()); // "0.1.1" +console.log(healthCheck()); // true + +// Coherence gate +const config = new GateConfigWasm(); +const gate = new ThresholdGateWasm(config); +const ctx = new GateContextWasm( + 42, 1, 1000000n, 15n, + '00000000000000000000000000000000', + 1.0, 0.1, RegimeLabelWasm.Calm, 10 +); +const decision = gate.evaluate(ctx); +console.log(decision.allowAct); // true +console.log(decision.allowLearn); // true + +// Market event +const evt = new MarketEventWasm(EventTypeWasm.Trade, 42, 1, 500000000n, 10000n); +const json = evt.toJson(); +const restored = MarketEventWasm.fromJson(json); + +// Replay memory +const store = new ReservoirStoreWasm(1000); +store.maybeWrite(segmentJson, decision.toJson()); +const results = store.retrieveBySymbol(42, 10); +``` + +## Graph Schema + +The order book is modeled as a **10-node-type, 12-edge-type heterogeneous dynamic graph** with 17 typed property keys. + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Symbol β”‚ + β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ + BELONGS_TO_SYMBOLβ”‚ ON_VENUE + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β–Ό β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β” + β”‚ Order β”‚ β”‚PriceLevelβ”‚ β”‚ Venue β”‚ + β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ + AT_LEVELβ”‚ NEXT_TICKβ”‚ + β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Trade β”‚ β”‚ Event β”‚ β”‚TimeBucket β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ + IN_REGIMEβ”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚Participantβ”‚ β”‚ Regime β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + AFFECTS_STATE + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚StrategyState β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Node properties** include: visible/hidden depth, queue length, local imbalance, refill/depletion rates, spread distance, realized volatility, cancel/fill hazard, slippage, post-trade impact, influence score, coherence contribution, and more. + +## Coherence Gate Flow + +``` +Market Events ──→ Graph Update ──→ Compute MinCut ──→ Gate Decision + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β–Ό β–Ό β–Ό + Retrieve Write Learn/Act + (permissive) (strict) (strictest) + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό + allow if allow if allow if + cut β‰₯ floor base_ok base_ok AND + drift < 50% max +``` + +**Tiered permissions** β€” retrieval is most permissive, learning is strictest: + +| Gate | Requires | +|------|----------| +| Retrieve | MinCut β‰₯ regime floor | +| Write/Act | MinCut + CUSUM OK + drift OK + boundary stable | +| Learn | All above + drift < half max (extra safety margin) | + +## Testing + +```bash +# All Rust tests (22 total across 4 crates) +cargo test -p neural-trader-core -p neural-trader-coherence \ + -p neural-trader-replay -p neural-trader-wasm + +# Node.js integration tests (43 tests) +cd crates/neural-trader-wasm && node tests/node-smoke.mjs + +# Build WASM (requires wasm-pack) +cd crates/neural-trader-wasm +CARGO_PROFILE_RELEASE_CODEGEN_UNITS=256 CARGO_PROFILE_RELEASE_LTO=off \ + wasm-pack build --target web --scope ruvector --release + +# Docker test +docker build -f crates/neural-trader-wasm/Dockerfile.test \ + -t neural-trader-wasm-test . +docker run --rm neural-trader-wasm-test +``` + +## Integration with RuVector Ecosystem + +| RuVector Component | Neural Trader Integration | +|-------------------|--------------------------| +| [ruvector-graph](../ruvector-graph) | Host graph for the dynamic market model | +| [ruvector-mincut](../ruvector-mincut) | MinCut computation for coherence gate | +| [ruvector-gnn](../ruvector-gnn) | GNN learning layer over market graph | +| [ruvector-attention](../ruvector-attention) | 46 attention mechanisms for temporal modeling | +| [ruvector-postgres](../ruvector-postgres) | Relational source of record (event log, embeddings, segments) | +| [ruvector-graph-transformer](../ruvector-graph-transformer) | Graph transformer with proof-gated mutation | +| [ruvector-coherence](../ruvector-coherence) | Coherence measurement for signal quality | +| [ruvector-verified](../ruvector-verified) | Formal proof substrate for gated writes | +| [ruvector-mincut-gated-transformer](../ruvector-mincut-gated-transformer) | Early exit + sparse compute during regime instability | + +## Roadmap + +- [ ] L2: Graph construction from order book snapshots (`neural-trader-graph`) +- [ ] L3: GNN embeddings with temporal attention (`neural-trader-gnn`) +- [ ] L6: Policy kernel with Kelly criterion position sizing (`neural-trader-policy`) +- [ ] Backtest harness with historical LOB data (`neural-trader-backtest`) +- [ ] WebSocket/FIX feed adapters (`neural-trader-feed`) +- [ ] PostgreSQL integration via `ruvector-postgres` SQL functions +- [ ] Streaming sketch memory (Count-Min, Top-K, delta histograms) +- [ ] Cognitum edge deployment (deterministic coherence gate kernel) + +## License + +MIT OR Apache-2.0 diff --git a/crates/neural-trader-core/src/lib.rs b/crates/neural-trader-core/src/lib.rs new file mode 100644 index 000000000..3390284bc --- /dev/null +++ b/crates/neural-trader-core/src/lib.rs @@ -0,0 +1,197 @@ +//! # neural-trader-core +//! +//! Canonical market event types, graph schema, and ingest traits for +//! the RuVector Neural Trader (ADR-084). + +use serde::{Deserialize, Serialize}; + +// --------------------------------------------------------------------------- +// Event types +// --------------------------------------------------------------------------- + +/// Canonical market event envelope. +/// +/// Every raw feed message is normalized into this structure before it +/// enters the graph or embedding pipeline. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MarketEvent { + pub event_id: [u8; 16], + pub ts_exchange_ns: u64, + pub ts_ingest_ns: u64, + pub venue_id: u16, + pub symbol_id: u32, + pub event_type: EventType, + pub side: Option, + /// Fixed-point price (e.g. price Γ— 1e8). + pub price_fp: i64, + /// Fixed-point quantity. + pub qty_fp: i64, + pub order_id_hash: Option<[u8; 16]>, + pub participant_id_hash: Option<[u8; 16]>, + pub flags: u32, + pub seq: u64, +} + +/// Discriminant for market event type. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[repr(u8)] +pub enum EventType { + NewOrder = 0, + ModifyOrder = 1, + CancelOrder = 2, + Trade = 3, + BookSnapshot = 4, + SessionMarker = 5, + VenueStatus = 6, +} + +/// Order side. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[repr(u8)] +pub enum Side { + Bid = 0, + Ask = 1, +} + +// --------------------------------------------------------------------------- +// Graph node and edge kinds +// --------------------------------------------------------------------------- + +/// Typed node kinds in the heterogeneous market graph. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(u8)] +pub enum NodeKind { + Symbol = 0, + Venue = 1, + PriceLevel = 2, + Order = 3, + Trade = 4, + Event = 5, + Participant = 6, + TimeBucket = 7, + Regime = 8, + StrategyState = 9, +} + +/// Typed edge kinds in the heterogeneous market graph. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(u8)] +pub enum EdgeKind { + AtLevel = 0, + NextTick = 1, + Generated = 2, + Matched = 3, + ModifiedFrom = 4, + CanceledBy = 5, + BelongsToSymbol = 6, + OnVenue = 7, + InWindow = 8, + CorrelatedWith = 9, + InRegime = 10, + AffectsState = 11, +} + +// --------------------------------------------------------------------------- +// Graph delta +// --------------------------------------------------------------------------- + +/// Property keys for graph node updates. +/// +/// Using an enum avoids heap-allocating strings on the hot ingest path. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(u8)] +pub enum PropertyKey { + VisibleDepth = 0, + EstimatedHiddenDepth = 1, + QueueLength = 2, + LocalImbalance = 3, + RefillRate = 4, + DepletionRate = 5, + SpreadDistance = 6, + LocalRealizedVol = 7, + CancelHazard = 8, + FillHazard = 9, + SlippageToMid = 10, + PostTradeImpact = 11, + InfluenceScore = 12, + CoherenceContribution = 13, + QueueEstimate = 14, + Age = 15, + ModifyCount = 16, +} + +/// Describes changes applied to the market graph after processing one event. +#[derive(Debug, Clone, Default)] +pub struct GraphDelta { + pub nodes_added: Vec<(NodeKind, u64)>, + pub edges_added: Vec<(EdgeKind, u64, u64)>, + pub properties_updated: Vec<(u64, PropertyKey, f64)>, +} + +// --------------------------------------------------------------------------- +// Traits +// --------------------------------------------------------------------------- + +/// Ingests normalized market events. +pub trait EventIngestor { + fn ingest(&mut self, event: MarketEvent) -> anyhow::Result<()>; +} + +/// Projects market events onto the dynamic graph. +pub trait GraphUpdater { + fn apply_event(&mut self, event: &MarketEvent) -> anyhow::Result; +} + +/// Produces vector embeddings from a state window. +pub trait Embedder { + fn embed_state(&self, ctx: &StateWindow) -> anyhow::Result>; +} + +/// A sliding window of graph state used for embedding. +#[derive(Debug, Clone)] +pub struct StateWindow { + pub symbol_id: u32, + pub venue_id: u16, + pub start_ns: u64, + pub end_ns: u64, + pub events: Vec, +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn market_event_roundtrip_json() { + let evt = MarketEvent { + event_id: [1u8; 16], + ts_exchange_ns: 1_000_000, + ts_ingest_ns: 1_000_100, + venue_id: 1, + symbol_id: 42, + event_type: EventType::Trade, + side: Some(Side::Bid), + price_fp: 500_000_000, + qty_fp: 10_000, + order_id_hash: None, + participant_id_hash: None, + flags: 0, + seq: 1, + }; + let json = serde_json::to_string(&evt).unwrap(); + let back: MarketEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(back.symbol_id, 42); + assert_eq!(back.event_type, EventType::Trade); + } + + #[test] + fn graph_delta_default_is_empty() { + let d = GraphDelta::default(); + assert!(d.nodes_added.is_empty()); + assert!(d.edges_added.is_empty()); + } +} diff --git a/crates/neural-trader-replay/Cargo.toml b/crates/neural-trader-replay/Cargo.toml new file mode 100644 index 000000000..1eb2cbd4d --- /dev/null +++ b/crates/neural-trader-replay/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "neural-trader-replay" +version = "0.1.0" +edition = "2021" +description = "Witnessable replay segments, RVF serialization, and audit receipt logging for Neural Trader" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +neural-trader-core = { path = "../neural-trader-core" } +neural-trader-coherence = { path = "../neural-trader-coherence" } + +[dev-dependencies] diff --git a/crates/neural-trader-replay/src/lib.rs b/crates/neural-trader-replay/src/lib.rs new file mode 100644 index 000000000..5c383e133 --- /dev/null +++ b/crates/neural-trader-replay/src/lib.rs @@ -0,0 +1,297 @@ +//! # neural-trader-replay +//! +//! Witnessable replay segments, RVF serialization stubs, and audit +//! receipt logging for the RuVector Neural Trader (ADR-084). + +use std::collections::VecDeque; + +use neural_trader_coherence::{CoherenceDecision, RegimeLabel, WitnessReceipt}; +use neural_trader_core::MarketEvent; +use serde::{Deserialize, Serialize}; + +// --------------------------------------------------------------------------- +// Replay segment +// --------------------------------------------------------------------------- + +/// A sealed, signed replay segment containing a compact subgraph window, +/// embeddings, labels, and coherence statistics. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReplaySegment { + pub segment_id: u64, + pub symbol_id: u32, + pub start_ts_ns: u64, + pub end_ts_ns: u64, + pub segment_kind: SegmentKind, + /// Serialized events in this window. + pub events: Vec, + /// Embedding snapshot at segment creation. + pub embedding: Option>, + /// Realized labels (e.g. mid-price move, fill outcome). + pub labels: serde_json::Value, + /// Coherence statistics at write time. + pub coherence_stats: CoherenceStats, + /// Lineage metadata. + pub lineage: SegmentLineage, + /// Witness hash for tamper detection. + pub witness_hash: [u8; 16], +} + +/// Segment classification. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum SegmentKind { + HighUncertainty, + LargeImpact, + RegimeTransition, + StructuralAnomaly, + RareQueuePattern, + HeadDisagreement, + Routine, +} + +/// Coherence statistics captured at segment write time. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CoherenceStats { + pub mincut_value: u64, + pub partition_hash: [u8; 16], + pub drift_score: f32, + pub cusum_score: f32, +} + +/// Lineage metadata linking a segment to its origin. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SegmentLineage { + pub model_id: String, + pub policy_version: String, + pub ingest_batch_id: Option, +} + +// --------------------------------------------------------------------------- +// Memory store trait +// --------------------------------------------------------------------------- + +/// Query for memory retrieval. +#[derive(Debug, Clone)] +pub struct MemoryQuery { + pub symbol_id: u32, + pub embedding: Vec, + pub regime: Option, + pub limit: usize, +} + +/// Selective, bounded memory store. +pub trait MemoryStore { + fn retrieve(&self, query: &MemoryQuery) -> anyhow::Result>; + + /// Attempts to write a segment. Returns `true` if the gate allowed + /// admission, `false` if rejected. + fn maybe_write( + &mut self, + seg: ReplaySegment, + gate: &CoherenceDecision, + ) -> anyhow::Result; +} + +// --------------------------------------------------------------------------- +// In-memory receipt log +// --------------------------------------------------------------------------- + +/// Simple in-memory witness logger for testing and research. +pub struct InMemoryReceiptLog { + pub receipts: Vec, +} + +impl InMemoryReceiptLog { + pub fn new() -> Self { + Self { + receipts: Vec::new(), + } + } + + pub fn len(&self) -> usize { + self.receipts.len() + } + + pub fn is_empty(&self) -> bool { + self.receipts.is_empty() + } +} + +impl Default for InMemoryReceiptLog { + fn default() -> Self { + Self::new() + } +} + +impl neural_trader_coherence::WitnessLogger for InMemoryReceiptLog { + fn append_receipt(&mut self, receipt: WitnessReceipt) -> anyhow::Result<()> { + self.receipts.push(receipt); + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// In-memory reservoir store +// --------------------------------------------------------------------------- + +/// Simple bounded reservoir memory store for research use. +/// +/// Uses `VecDeque` for O(1) front eviction instead of `Vec::remove(0)` O(n). +pub struct ReservoirStore { + pub segments: VecDeque, + pub max_size: usize, +} + +impl ReservoirStore { + pub fn new(max_size: usize) -> Self { + Self { + segments: VecDeque::with_capacity(max_size.min(1024)), + max_size, + } + } + + pub fn len(&self) -> usize { + self.segments.len() + } + + pub fn is_empty(&self) -> bool { + self.segments.is_empty() + } +} + +impl MemoryStore for ReservoirStore { + fn retrieve(&self, query: &MemoryQuery) -> anyhow::Result> { + let results: Vec<_> = self + .segments + .iter() + .filter(|s| s.symbol_id == query.symbol_id) + .take(query.limit) + .cloned() + .collect(); + Ok(results) + } + + fn maybe_write( + &mut self, + seg: ReplaySegment, + gate: &CoherenceDecision, + ) -> anyhow::Result { + if !gate.allow_write { + return Ok(false); + } + if self.segments.len() >= self.max_size { + self.segments.pop_front(); // O(1) eviction + } + self.segments.push_back(seg); + Ok(true) + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use neural_trader_coherence::WitnessLogger; + + #[test] + fn receipt_log_append() { + let mut log = InMemoryReceiptLog::new(); + assert!(log.is_empty()); + + let receipt = WitnessReceipt { + ts_ns: 1_000_000, + model_id: "test-v1".into(), + input_segment_hash: [0u8; 16], + coherence_witness_hash: [1u8; 16], + policy_hash: [2u8; 16], + action_intent: "place_bid".into(), + verified_token_id: [3u8; 16], + resulting_state_hash: [4u8; 16], + }; + log.append_receipt(receipt).unwrap(); + assert_eq!(log.len(), 1); + } + + #[test] + fn reservoir_respects_gate() { + let mut store = ReservoirStore::new(100); + let seg = make_test_segment(1); + + let blocked = CoherenceDecision { + allow_retrieve: true, + allow_write: false, + allow_learn: false, + allow_act: false, + mincut_value: 3, + partition_hash: [0u8; 16], + drift_score: 0.9, + cusum_score: 5.0, + reasons: vec!["test block".into()], + }; + assert!(!store.maybe_write(seg.clone(), &blocked).unwrap()); + assert!(store.is_empty()); + + let allowed = CoherenceDecision { + allow_retrieve: true, + allow_write: true, + allow_learn: true, + allow_act: true, + mincut_value: 15, + partition_hash: [0u8; 16], + drift_score: 0.1, + cusum_score: 1.0, + reasons: vec![], + }; + assert!(store.maybe_write(seg, &allowed).unwrap()); + assert_eq!(store.len(), 1); + } + + #[test] + fn reservoir_evicts_when_full() { + let mut store = ReservoirStore::new(2); + let gate = CoherenceDecision { + allow_retrieve: true, + allow_write: true, + allow_learn: true, + allow_act: true, + mincut_value: 15, + partition_hash: [0u8; 16], + drift_score: 0.1, + cusum_score: 1.0, + reasons: vec![], + }; + store.maybe_write(make_test_segment(1), &gate).unwrap(); + store.maybe_write(make_test_segment(2), &gate).unwrap(); + store.maybe_write(make_test_segment(3), &gate).unwrap(); + assert_eq!(store.len(), 2); + // First segment (id=1) was evicted via O(1) pop_front. + assert_eq!(store.segments.front().unwrap().segment_id, 2); + } + + fn make_test_segment(id: u64) -> ReplaySegment { + ReplaySegment { + segment_id: id, + symbol_id: 42, + start_ts_ns: 0, + end_ts_ns: 1_000_000, + segment_kind: SegmentKind::Routine, + events: vec![], + embedding: None, + labels: serde_json::json!({}), + coherence_stats: CoherenceStats { + mincut_value: 10, + partition_hash: [0u8; 16], + drift_score: 0.1, + cusum_score: 1.0, + }, + lineage: SegmentLineage { + model_id: "test".into(), + policy_version: "v1".into(), + ingest_batch_id: None, + }, + witness_hash: [0u8; 16], + } + } +} diff --git a/crates/neural-trader-wasm/Cargo.toml b/crates/neural-trader-wasm/Cargo.toml new file mode 100644 index 000000000..cc97e8a5e --- /dev/null +++ b/crates/neural-trader-wasm/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "neural-trader-wasm" +version = "0.1.1" +edition = "2021" +license = "MIT" +publish = false +description = "WASM bindings for Neural Trader β€” market events, coherence gates, replay memory" + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +neural-trader-core = { path = "../neural-trader-core" } +neural-trader-coherence = { path = "../neural-trader-coherence" } +neural-trader-replay = { path = "../neural-trader-replay" } +wasm-bindgen = { workspace = true } +serde = { workspace = true } +serde-wasm-bindgen = "0.6" +console_error_panic_hook = { version = "0.1", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3" + +[features] +default = ["console_error_panic_hook"] diff --git a/crates/neural-trader-wasm/Dockerfile.test b/crates/neural-trader-wasm/Dockerfile.test new file mode 100644 index 000000000..a6f02f2da --- /dev/null +++ b/crates/neural-trader-wasm/Dockerfile.test @@ -0,0 +1,60 @@ +## Docker test runner for neural-trader-wasm +## Validates WASM module loads and exports are correct in Node 20. +## +## Build: docker build -f crates/neural-trader-wasm/Dockerfile.test -t neural-trader-wasm-test . +## Run: docker run --rm neural-trader-wasm-test + +FROM node:20-slim AS base + +WORKDIR /app + +# ---------- stage 1: build WASM from source ---------- +FROM rust:1.83-bookworm AS builder + +RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + +WORKDIR /src + +# Copy only the crates we need +COPY crates/neural-trader-core ./crates/neural-trader-core +COPY crates/neural-trader-coherence ./crates/neural-trader-coherence +COPY crates/neural-trader-replay ./crates/neural-trader-replay +COPY crates/neural-trader-wasm ./crates/neural-trader-wasm + +# Generate a minimal workspace Cargo.toml (avoids copying all 87 workspace members) +RUN cat > Cargo.toml <<'TOML' +[workspace] +members = [ + "crates/neural-trader-core", + "crates/neural-trader-coherence", + "crates/neural-trader-replay", + "crates/neural-trader-wasm", +] +resolver = "2" + +[workspace.dependencies] +wasm-bindgen = "0.2" +serde = { version = "1.0", features = ["derive"] } + +[profile.release] +opt-level = 3 +lto = "fat" +codegen-units = 1 +strip = true +panic = "unwind" +TOML + +# Build WASM +RUN cd crates/neural-trader-wasm && \ + CARGO_PROFILE_RELEASE_CODEGEN_UNITS=256 CARGO_PROFILE_RELEASE_LTO=off \ + wasm-pack build --target web --scope ruvector --release + +# ---------- stage 2: test in Node ---------- +FROM base + +COPY --from=builder /src/crates/neural-trader-wasm/pkg /app/pkg +COPY crates/neural-trader-wasm/tests/node-smoke.mjs /app/tests/node-smoke.mjs + +RUN echo '{"type":"module"}' > /app/package.json + +CMD ["node", "--experimental-vm-modules", "/app/tests/node-smoke.mjs"] diff --git a/crates/neural-trader-wasm/src/lib.rs b/crates/neural-trader-wasm/src/lib.rs new file mode 100644 index 000000000..2c564d675 --- /dev/null +++ b/crates/neural-trader-wasm/src/lib.rs @@ -0,0 +1,879 @@ +//! # neural-trader-wasm +//! +//! WASM bindings for the Neural Trader crates: market events, +//! coherence gates, and replay memory (ADR-085 / ADR-086). + +use serde::Serialize; +use wasm_bindgen::prelude::*; + +use neural_trader_coherence::{ + CoherenceDecision, CoherenceGate, GateConfig, GateContext, RegimeLabel, ThresholdGate, +}; +use neural_trader_core::{EventType, MarketEvent, Side}; +use neural_trader_replay::{MemoryStore, ReplaySegment, ReservoirStore}; + +// --------------------------------------------------------------------------- +// Init & utilities +// --------------------------------------------------------------------------- + +#[wasm_bindgen(start)] +pub fn init() { + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} + +/// Returns the crate version. +#[wasm_bindgen] +pub fn version() -> String { + env!("CARGO_PKG_VERSION").to_string() +} + +/// Smoke-test that the WASM module loaded correctly. +#[wasm_bindgen(js_name = "healthCheck")] +pub fn health_check() -> bool { + true +} + +// --------------------------------------------------------------------------- +// Hex helpers for [u8; 16] +// --------------------------------------------------------------------------- + +fn bytes16_to_hex(b: &[u8; 16]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +fn hex_to_bytes16_inner(s: &str) -> Result<[u8; 16], String> { + let s = s.trim(); + // Strip optional 0x prefix for JS ergonomics. + let s = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s); + if !s.is_ascii() || s.len() != 32 { + return Err( + "hex string must be exactly 32 ASCII hex chars (optional 0x prefix)".to_string(), + ); + } + let mut out = [0u8; 16]; + for (i, byte) in out.iter_mut().enumerate() { + *byte = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16) + .map_err(|e| e.to_string())?; + } + Ok(out) +} + +fn hex_to_bytes16(s: &str) -> Result<[u8; 16], JsValue> { + hex_to_bytes16_inner(s).map_err(|e| JsValue::from_str(&e)) +} + +/// Serialize using BigInt-aware serializer to avoid u64 precision loss. +fn to_js(v: &T) -> Result { + let ser = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); + v.serialize(&ser).map_err(|e| JsValue::from_str(&e.to_string())) +} + +// --------------------------------------------------------------------------- +// Enum conversion macro +// --------------------------------------------------------------------------- + +macro_rules! enum_convert { + ($wasm:ident <=> $inner:path { $($variant:ident),+ $(,)? }) => { + impl From<$inner> for $wasm { + fn from(v: $inner) -> Self { + match v { $( <$inner>::$variant => $wasm::$variant, )+ } + } + } + impl From<$wasm> for $inner { + fn from(v: $wasm) -> Self { + match v { $( $wasm::$variant => <$inner>::$variant, )+ } + } + } + }; +} + +// --------------------------------------------------------------------------- +// Enums +// --------------------------------------------------------------------------- + +#[wasm_bindgen] +#[derive(Clone, Copy, Debug)] +pub enum EventTypeWasm { + NewOrder = 0, + ModifyOrder = 1, + CancelOrder = 2, + Trade = 3, + BookSnapshot = 4, + SessionMarker = 5, + VenueStatus = 6, +} +enum_convert!(EventTypeWasm <=> EventType { + NewOrder, ModifyOrder, CancelOrder, Trade, BookSnapshot, SessionMarker, VenueStatus +}); + +#[wasm_bindgen] +#[derive(Clone, Copy, Debug)] +pub enum SideWasm { + Bid = 0, + Ask = 1, +} +enum_convert!(SideWasm <=> Side { Bid, Ask }); + +#[wasm_bindgen] +#[derive(Clone, Copy, Debug)] +pub enum RegimeLabelWasm { + Calm = 0, + Normal = 1, + Volatile = 2, +} +enum_convert!(RegimeLabelWasm <=> RegimeLabel { Calm, Normal, Volatile }); + +#[wasm_bindgen] +#[derive(Clone, Copy, Debug)] +pub enum SegmentKindWasm { + HighUncertainty = 0, + LargeImpact = 1, + RegimeTransition = 2, + StructuralAnomaly = 3, + RareQueuePattern = 4, + HeadDisagreement = 5, + Routine = 6, +} +enum_convert!(SegmentKindWasm <=> neural_trader_replay::SegmentKind { + HighUncertainty, LargeImpact, RegimeTransition, StructuralAnomaly, + RareQueuePattern, HeadDisagreement, Routine +}); + +#[wasm_bindgen] +#[derive(Clone, Copy, Debug)] +pub enum NodeKindWasm { + Symbol = 0, Venue = 1, PriceLevel = 2, Order = 3, Trade = 4, + Event = 5, Participant = 6, TimeBucket = 7, Regime = 8, StrategyState = 9, +} +enum_convert!(NodeKindWasm <=> neural_trader_core::NodeKind { + Symbol, Venue, PriceLevel, Order, Trade, Event, Participant, + TimeBucket, Regime, StrategyState +}); + +#[wasm_bindgen] +#[derive(Clone, Copy, Debug)] +pub enum EdgeKindWasm { + AtLevel = 0, NextTick = 1, Generated = 2, Matched = 3, ModifiedFrom = 4, + CanceledBy = 5, BelongsToSymbol = 6, OnVenue = 7, InWindow = 8, + CorrelatedWith = 9, InRegime = 10, AffectsState = 11, +} +enum_convert!(EdgeKindWasm <=> neural_trader_core::EdgeKind { + AtLevel, NextTick, Generated, Matched, ModifiedFrom, CanceledBy, + BelongsToSymbol, OnVenue, InWindow, CorrelatedWith, InRegime, AffectsState +}); + +#[wasm_bindgen] +#[derive(Clone, Copy, Debug)] +pub enum PropertyKeyWasm { + VisibleDepth = 0, + EstimatedHiddenDepth = 1, + QueueLength = 2, + LocalImbalance = 3, + RefillRate = 4, + DepletionRate = 5, + SpreadDistance = 6, + LocalRealizedVol = 7, + CancelHazard = 8, + FillHazard = 9, + SlippageToMid = 10, + PostTradeImpact = 11, + InfluenceScore = 12, + CoherenceContribution = 13, + QueueEstimate = 14, + Age = 15, + ModifyCount = 16, +} +enum_convert!(PropertyKeyWasm <=> neural_trader_core::PropertyKey { + VisibleDepth, EstimatedHiddenDepth, QueueLength, LocalImbalance, + RefillRate, DepletionRate, SpreadDistance, LocalRealizedVol, + CancelHazard, FillHazard, SlippageToMid, PostTradeImpact, + InfluenceScore, CoherenceContribution, QueueEstimate, Age, ModifyCount +}); + +// --------------------------------------------------------------------------- +// MarketEventWasm +// --------------------------------------------------------------------------- + +#[wasm_bindgen] +pub struct MarketEventWasm { + inner: MarketEvent, +} + +#[wasm_bindgen] +impl MarketEventWasm { + #[wasm_bindgen(constructor)] + pub fn new( + event_type: EventTypeWasm, + symbol_id: u32, + venue_id: u16, + price_fp: i64, + qty_fp: i64, + ) -> Self { + Self { + inner: MarketEvent { + event_id: [0u8; 16], + ts_exchange_ns: 0, + ts_ingest_ns: 0, + venue_id, + symbol_id, + event_type: event_type.into(), + side: None, + price_fp, + qty_fp, + order_id_hash: None, + participant_id_hash: None, + flags: 0, + seq: 0, + }, + } + } + + // --- Getters --- + + #[wasm_bindgen(getter, js_name = "eventId")] + pub fn event_id(&self) -> String { + bytes16_to_hex(&self.inner.event_id) + } + + #[wasm_bindgen(getter, js_name = "tsExchangeNs")] + pub fn ts_exchange_ns(&self) -> u64 { + self.inner.ts_exchange_ns + } + + #[wasm_bindgen(getter, js_name = "tsIngestNs")] + pub fn ts_ingest_ns(&self) -> u64 { + self.inner.ts_ingest_ns + } + + #[wasm_bindgen(getter, js_name = "venueId")] + pub fn venue_id(&self) -> u16 { + self.inner.venue_id + } + + #[wasm_bindgen(getter, js_name = "symbolId")] + pub fn symbol_id(&self) -> u32 { + self.inner.symbol_id + } + + #[wasm_bindgen(getter, js_name = "eventType")] + pub fn event_type(&self) -> EventTypeWasm { + self.inner.event_type.into() + } + + #[wasm_bindgen(getter)] + pub fn side(&self) -> Option { + self.inner.side.map(|s| s.into()) + } + + #[wasm_bindgen(getter, js_name = "priceFp")] + pub fn price_fp(&self) -> i64 { + self.inner.price_fp + } + + #[wasm_bindgen(getter, js_name = "qtyFp")] + pub fn qty_fp(&self) -> i64 { + self.inner.qty_fp + } + + #[wasm_bindgen(getter)] + pub fn flags(&self) -> u32 { + self.inner.flags + } + + #[wasm_bindgen(getter)] + pub fn seq(&self) -> u64 { + self.inner.seq + } + + // --- Setters --- + + #[wasm_bindgen(setter, js_name = "eventId")] + pub fn set_event_id(&mut self, hex: &str) -> Result<(), JsValue> { + self.inner.event_id = hex_to_bytes16(hex)?; + Ok(()) + } + + #[wasm_bindgen(setter, js_name = "tsExchangeNs")] + pub fn set_ts_exchange_ns(&mut self, v: u64) { + self.inner.ts_exchange_ns = v; + } + + #[wasm_bindgen(setter, js_name = "tsIngestNs")] + pub fn set_ts_ingest_ns(&mut self, v: u64) { + self.inner.ts_ingest_ns = v; + } + + #[wasm_bindgen(setter)] + pub fn set_side(&mut self, side: SideWasm) { + self.inner.side = Some(side.into()); + } + + #[wasm_bindgen(setter)] + pub fn set_flags(&mut self, v: u32) { + self.inner.flags = v; + } + + #[wasm_bindgen(setter)] + pub fn set_seq(&mut self, v: u64) { + self.inner.seq = v; + } + + #[wasm_bindgen(js_name = "setOrderIdHash")] + pub fn set_order_id_hash(&mut self, hex: &str) -> Result<(), JsValue> { + self.inner.order_id_hash = Some(hex_to_bytes16(hex)?); + Ok(()) + } + + #[wasm_bindgen(js_name = "setParticipantIdHash")] + pub fn set_participant_id_hash(&mut self, hex: &str) -> Result<(), JsValue> { + self.inner.participant_id_hash = Some(hex_to_bytes16(hex)?); + Ok(()) + } + + // --- JSON round-trip (BigInt-safe) --- + + #[wasm_bindgen(js_name = "toJson")] + pub fn to_json(&self) -> Result { + to_js(&self.inner) + } + + #[wasm_bindgen(js_name = "fromJson")] + pub fn from_json(val: JsValue) -> Result { + let inner: MarketEvent = + serde_wasm_bindgen::from_value(val).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(Self { inner }) + } +} + +// --------------------------------------------------------------------------- +// GraphDeltaWasm +// --------------------------------------------------------------------------- + +#[wasm_bindgen] +pub struct GraphDeltaWasm { + inner: neural_trader_core::GraphDelta, +} + +#[wasm_bindgen] +impl GraphDeltaWasm { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + inner: neural_trader_core::GraphDelta::default(), + } + } + + #[wasm_bindgen(js_name = "nodesAdded")] + pub fn nodes_added(&self) -> Result { + let pairs: Vec<(u8, u64)> = self + .inner + .nodes_added + .iter() + .map(|(k, id)| (*k as u8, *id)) + .collect(); + to_js(&pairs) + } + + #[wasm_bindgen(js_name = "edgesAdded")] + pub fn edges_added(&self) -> Result { + let triples: Vec<(u8, u64, u64)> = self + .inner + .edges_added + .iter() + .map(|(k, src, dst)| (*k as u8, *src, *dst)) + .collect(); + to_js(&triples) + } + + #[wasm_bindgen(js_name = "propertiesUpdated")] + pub fn properties_updated(&self) -> Result { + let props: Vec<(u64, u8, f64)> = self + .inner + .properties_updated + .iter() + .map(|(id, k, v)| (*id, *k as u8, *v)) + .collect(); + to_js(&props) + } +} + +impl Default for GraphDeltaWasm { + fn default() -> Self { + Self::new() + } +} + +// --------------------------------------------------------------------------- +// GateConfigWasm +// --------------------------------------------------------------------------- + +#[wasm_bindgen] +pub struct GateConfigWasm { + inner: GateConfig, +} + +#[wasm_bindgen] +impl GateConfigWasm { + /// Creates a `GateConfig` with sensible defaults. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + inner: GateConfig::default(), + } + } + + #[wasm_bindgen(getter, js_name = "mincutFloorCalm")] + pub fn mincut_floor_calm(&self) -> u64 { + self.inner.mincut_floor_calm + } + #[wasm_bindgen(setter, js_name = "mincutFloorCalm")] + pub fn set_mincut_floor_calm(&mut self, v: u64) { + self.inner.mincut_floor_calm = v; + } + + #[wasm_bindgen(getter, js_name = "mincutFloorNormal")] + pub fn mincut_floor_normal(&self) -> u64 { + self.inner.mincut_floor_normal + } + #[wasm_bindgen(setter, js_name = "mincutFloorNormal")] + pub fn set_mincut_floor_normal(&mut self, v: u64) { + self.inner.mincut_floor_normal = v; + } + + #[wasm_bindgen(getter, js_name = "mincutFloorVolatile")] + pub fn mincut_floor_volatile(&self) -> u64 { + self.inner.mincut_floor_volatile + } + #[wasm_bindgen(setter, js_name = "mincutFloorVolatile")] + pub fn set_mincut_floor_volatile(&mut self, v: u64) { + self.inner.mincut_floor_volatile = v; + } + + #[wasm_bindgen(getter, js_name = "cusumThreshold")] + pub fn cusum_threshold(&self) -> f32 { + self.inner.cusum_threshold + } + #[wasm_bindgen(setter, js_name = "cusumThreshold")] + pub fn set_cusum_threshold(&mut self, v: f32) { + self.inner.cusum_threshold = v; + } + + #[wasm_bindgen(getter, js_name = "boundaryStabilityWindows")] + pub fn boundary_stability_windows(&self) -> usize { + self.inner.boundary_stability_windows + } + #[wasm_bindgen(setter, js_name = "boundaryStabilityWindows")] + pub fn set_boundary_stability_windows(&mut self, v: usize) { + self.inner.boundary_stability_windows = v; + } + + #[wasm_bindgen(getter, js_name = "maxDriftScore")] + pub fn max_drift_score(&self) -> f32 { + self.inner.max_drift_score + } + #[wasm_bindgen(setter, js_name = "maxDriftScore")] + pub fn set_max_drift_score(&mut self, v: f32) { + self.inner.max_drift_score = v; + } +} + +impl Default for GateConfigWasm { + fn default() -> Self { + Self::new() + } +} + +// --------------------------------------------------------------------------- +// GateContextWasm +// --------------------------------------------------------------------------- + +#[wasm_bindgen] +pub struct GateContextWasm { + symbol_id: u32, + venue_id: u16, + ts_ns: u64, + mincut_value: u64, + partition_hash: String, + cusum_score: f32, + drift_score: f32, + regime: RegimeLabelWasm, + boundary_stable_count: usize, +} + +#[wasm_bindgen] +impl GateContextWasm { + #[wasm_bindgen(constructor)] + #[allow(clippy::too_many_arguments)] + pub fn new( + symbol_id: u32, + venue_id: u16, + ts_ns: u64, + mincut_value: u64, + partition_hash: &str, + cusum_score: f32, + drift_score: f32, + regime: RegimeLabelWasm, + boundary_stable_count: usize, + ) -> Self { + Self { + symbol_id, + venue_id, + ts_ns, + mincut_value, + partition_hash: partition_hash.to_string(), + cusum_score, + drift_score, + regime, + boundary_stable_count, + } + } + + #[wasm_bindgen(getter, js_name = "symbolId")] + pub fn symbol_id(&self) -> u32 { + self.symbol_id + } + #[wasm_bindgen(getter, js_name = "venueId")] + pub fn venue_id(&self) -> u16 { + self.venue_id + } + #[wasm_bindgen(getter, js_name = "tsNs")] + pub fn ts_ns(&self) -> u64 { + self.ts_ns + } + #[wasm_bindgen(getter, js_name = "mincutValue")] + pub fn mincut_value(&self) -> u64 { + self.mincut_value + } + #[wasm_bindgen(getter, js_name = "partitionHash")] + pub fn partition_hash(&self) -> String { + self.partition_hash.clone() + } + #[wasm_bindgen(getter, js_name = "cusumScore")] + pub fn cusum_score(&self) -> f32 { + self.cusum_score + } + #[wasm_bindgen(getter, js_name = "driftScore")] + pub fn drift_score(&self) -> f32 { + self.drift_score + } + #[wasm_bindgen(getter)] + pub fn regime(&self) -> RegimeLabelWasm { + self.regime + } + #[wasm_bindgen(getter, js_name = "boundaryStableCount")] + pub fn boundary_stable_count(&self) -> usize { + self.boundary_stable_count + } + + fn to_inner(&self) -> Result { + Ok(GateContext { + symbol_id: self.symbol_id, + venue_id: self.venue_id, + ts_ns: self.ts_ns, + mincut_value: self.mincut_value, + partition_hash: hex_to_bytes16(&self.partition_hash)?, + cusum_score: self.cusum_score, + drift_score: self.drift_score, + regime: self.regime.into(), + boundary_stable_count: self.boundary_stable_count, + }) + } +} + +// --------------------------------------------------------------------------- +// ThresholdGateWasm +// --------------------------------------------------------------------------- + +#[wasm_bindgen] +pub struct ThresholdGateWasm { + inner: ThresholdGate, +} + +#[wasm_bindgen] +impl ThresholdGateWasm { + #[wasm_bindgen(constructor)] + pub fn new(config: &GateConfigWasm) -> Self { + Self { + inner: ThresholdGate::new(config.inner.clone()), + } + } + + /// Evaluate the coherence gate, returning a decision. + pub fn evaluate(&self, ctx: &GateContextWasm) -> Result { + let inner_ctx = ctx.to_inner()?; + let decision = self + .inner + .evaluate(&inner_ctx) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(CoherenceDecisionWasm { inner: decision }) + } +} + +// --------------------------------------------------------------------------- +// CoherenceDecisionWasm +// --------------------------------------------------------------------------- + +#[wasm_bindgen] +pub struct CoherenceDecisionWasm { + inner: CoherenceDecision, +} + +#[wasm_bindgen] +impl CoherenceDecisionWasm { + #[wasm_bindgen(getter, js_name = "allowRetrieve")] + pub fn allow_retrieve(&self) -> bool { + self.inner.allow_retrieve + } + + #[wasm_bindgen(getter, js_name = "allowWrite")] + pub fn allow_write(&self) -> bool { + self.inner.allow_write + } + + #[wasm_bindgen(getter, js_name = "allowLearn")] + pub fn allow_learn(&self) -> bool { + self.inner.allow_learn + } + + #[wasm_bindgen(getter, js_name = "allowAct")] + pub fn allow_act(&self) -> bool { + self.inner.allow_act + } + + #[wasm_bindgen(getter, js_name = "mincutValue")] + pub fn mincut_value(&self) -> u64 { + self.inner.mincut_value + } + + #[wasm_bindgen(getter, js_name = "partitionHash")] + pub fn partition_hash(&self) -> String { + bytes16_to_hex(&self.inner.partition_hash) + } + + #[wasm_bindgen(getter, js_name = "driftScore")] + pub fn drift_score(&self) -> f32 { + self.inner.drift_score + } + + #[wasm_bindgen(getter, js_name = "cusumScore")] + pub fn cusum_score(&self) -> f32 { + self.inner.cusum_score + } + + #[wasm_bindgen(js_name = "allAllowed")] + pub fn all_allowed(&self) -> bool { + self.inner.all_allowed() + } + + #[wasm_bindgen(js_name = "fullyBlocked")] + pub fn fully_blocked(&self) -> bool { + self.inner.fully_blocked() + } + + pub fn reasons(&self) -> Result { + to_js(&self.inner.reasons) + } + + #[wasm_bindgen(js_name = "toJson")] + pub fn to_json(&self) -> Result { + to_js(&self.inner) + } +} + +// --------------------------------------------------------------------------- +// ReplaySegmentWasm +// --------------------------------------------------------------------------- + +#[wasm_bindgen] +pub struct ReplaySegmentWasm { + inner: ReplaySegment, +} + +#[wasm_bindgen] +impl ReplaySegmentWasm { + #[wasm_bindgen(getter, js_name = "segmentId")] + pub fn segment_id(&self) -> u64 { + self.inner.segment_id + } + + #[wasm_bindgen(getter, js_name = "symbolId")] + pub fn symbol_id(&self) -> u32 { + self.inner.symbol_id + } + + #[wasm_bindgen(getter, js_name = "startTsNs")] + pub fn start_ts_ns(&self) -> u64 { + self.inner.start_ts_ns + } + + #[wasm_bindgen(getter, js_name = "endTsNs")] + pub fn end_ts_ns(&self) -> u64 { + self.inner.end_ts_ns + } + + /// BigInt-safe JSON serialization. + #[wasm_bindgen(js_name = "toJson")] + pub fn to_json(&self) -> Result { + to_js(&self.inner) + } + + #[wasm_bindgen(js_name = "fromJson")] + pub fn from_json(val: JsValue) -> Result { + let inner: ReplaySegment = + serde_wasm_bindgen::from_value(val).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(Self { inner }) + } +} + +// --------------------------------------------------------------------------- +// ReservoirStoreWasm +// --------------------------------------------------------------------------- + +#[wasm_bindgen] +pub struct ReservoirStoreWasm { + inner: ReservoirStore, +} + +#[wasm_bindgen] +impl ReservoirStoreWasm { + #[wasm_bindgen(constructor)] + pub fn new(max_size: usize) -> Result { + if max_size == 0 { + return Err(JsValue::from_str("max_size must be > 0")); + } + Ok(Self { + inner: ReservoirStore::new(max_size), + }) + } + + pub fn len(&self) -> usize { + self.inner.len() + } + + #[wasm_bindgen(js_name = "isEmpty")] + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Attempt to write a segment. Returns `true` if the gate allowed it. + #[wasm_bindgen(js_name = "maybeWrite")] + pub fn maybe_write( + &mut self, + segment_json: JsValue, + decision_json: JsValue, + ) -> Result { + let seg: ReplaySegment = serde_wasm_bindgen::from_value(segment_json) + .map_err(|e| JsValue::from_str(&format!("segment: {e}")))?; + let gate: CoherenceDecision = serde_wasm_bindgen::from_value(decision_json) + .map_err(|e| JsValue::from_str(&format!("decision: {e}")))?; + self.inner + .maybe_write(seg, &gate) + .map_err(|e| JsValue::from_str(&e.to_string())) + } + + /// Retrieve segments matching a symbol, returned as JSON array. + #[wasm_bindgen(js_name = "retrieveBySymbol")] + pub fn retrieve_by_symbol( + &self, + symbol_id: u32, + limit: usize, + ) -> Result { + let query = neural_trader_replay::MemoryQuery { + symbol_id, + embedding: vec![], + regime: None, + limit, + }; + let results = self + .inner + .retrieve(&query) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + to_js(&results) + } +} + +// --------------------------------------------------------------------------- +// Tests (rlib target only) +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn version_not_empty() { + assert!(!version().is_empty()); + } + + #[test] + fn health_check_returns_true() { + assert!(health_check()); + } + + #[test] + fn hex_roundtrip() { + let orig = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + let hex = bytes16_to_hex(&orig); + let back = hex_to_bytes16_inner(&hex).unwrap(); + assert_eq!(orig, back); + } + + #[test] + fn hex_rejects_non_ascii() { + let non_ascii = "\u{00e9}".repeat(16); + assert!(hex_to_bytes16_inner(&non_ascii).is_err()); + } + + #[test] + fn hex_strips_0x_prefix() { + let orig = [0xABu8, 0xCD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let hex = format!("0x{}", bytes16_to_hex(&orig)); + let back = hex_to_bytes16_inner(&hex).unwrap(); + assert_eq!(orig, back); + } + + #[test] + fn hex_rejects_wrong_length() { + assert!(hex_to_bytes16_inner("abcd").is_err()); + assert!(hex_to_bytes16_inner("").is_err()); + } + + #[test] + fn gate_config_defaults() { + let cfg = GateConfigWasm::new(); + assert_eq!(cfg.mincut_floor_calm(), 12); + assert_eq!(cfg.mincut_floor_normal(), 9); + assert_eq!(cfg.mincut_floor_volatile(), 6); + } + + #[test] + fn market_event_basic() { + let evt = MarketEventWasm::new(EventTypeWasm::Trade, 42, 1, 500_000_000, 10_000); + assert_eq!(evt.symbol_id(), 42); + assert_eq!(evt.venue_id(), 1); + assert_eq!(evt.price_fp(), 500_000_000); + assert!(evt.side().is_none()); + } + + #[test] + fn enum_conversions() { + let et: EventType = EventTypeWasm::Trade.into(); + assert_eq!(et, EventType::Trade); + + let back: EventTypeWasm = et.into(); + assert_eq!(back as u8, EventTypeWasm::Trade as u8); + + let rl: RegimeLabel = RegimeLabelWasm::Volatile.into(); + assert_eq!(rl, RegimeLabel::Volatile); + } + + #[test] + fn property_key_conversions() { + let pk: neural_trader_core::PropertyKey = PropertyKeyWasm::CancelHazard.into(); + assert_eq!(pk, neural_trader_core::PropertyKey::CancelHazard); + } + + // ReservoirStoreWasm::new(0) returns Err(JsValue) which panics in + // native tests. The zero-size guard is exercised in WASM integration tests. +} diff --git a/crates/neural-trader-wasm/tests/node-smoke.mjs b/crates/neural-trader-wasm/tests/node-smoke.mjs new file mode 100644 index 000000000..0b458202f --- /dev/null +++ b/crates/neural-trader-wasm/tests/node-smoke.mjs @@ -0,0 +1,170 @@ +/** + * Node.js smoke test for @ruvector/neural-trader-wasm + * + * Validates: module loads, all exports exist, coherence gate round-trip, + * replay store read/write, and BigInt timestamp fidelity. + * + * Run: node --experimental-vm-modules tests/node-smoke.mjs + * Or: node tests/node-smoke.mjs (Node 22+) + */ + +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const pkgDir = join(__dir, '..', 'pkg'); + +let pass = 0; +let fail = 0; + +function assert(cond, msg) { + if (cond) { + pass++; + console.log(` βœ“ ${msg}`); + } else { + fail++; + console.error(` βœ— ${msg}`); + } +} + +function assertEqual(a, b, msg) { + assert(a === b, `${msg} (got ${a}, expected ${b})`); +} + +// ---------- Load WASM ---------- +// For Node we use initSync with a buffer instead of fetch. +const jsPath = join(pkgDir, 'neural_trader_wasm.js'); +const wasmPath = join(pkgDir, 'neural_trader_wasm_bg.wasm'); + +const wasmMod = await import(jsPath); +const wasmBytes = await readFile(wasmPath); +wasmMod.initSync({ module: wasmBytes }); + +const { + version, + healthCheck, + GateConfigWasm, + ThresholdGateWasm, + GateContextWasm, + CoherenceDecisionWasm, + MarketEventWasm, + GraphDeltaWasm, + ReservoirStoreWasm, + ReplaySegmentWasm, + EventTypeWasm, + SideWasm, + RegimeLabelWasm, + SegmentKindWasm, + NodeKindWasm, + EdgeKindWasm, + PropertyKeyWasm, +} = wasmMod; + +// ---------- Tests ---------- + +console.log('\n--- Utilities ---'); +assert(typeof version() === 'string' && version().length > 0, 'version() returns non-empty string'); +assertEqual(healthCheck(), true, 'healthCheck() returns true'); + +console.log('\n--- Enums ---'); +assertEqual(EventTypeWasm.Trade, 3, 'EventTypeWasm.Trade = 3'); +assertEqual(SideWasm.Bid, 0, 'SideWasm.Bid = 0'); +assertEqual(RegimeLabelWasm.Volatile, 2, 'RegimeLabelWasm.Volatile = 2'); +assertEqual(SegmentKindWasm.Routine, 6, 'SegmentKindWasm.Routine = 6'); +assertEqual(NodeKindWasm.Symbol, 0, 'NodeKindWasm.Symbol = 0'); +assertEqual(EdgeKindWasm.Matched, 3, 'EdgeKindWasm.Matched = 3'); +assertEqual(PropertyKeyWasm.CancelHazard, 8, 'PropertyKeyWasm.CancelHazard = 8'); + +console.log('\n--- MarketEvent ---'); +const evt = new MarketEventWasm(EventTypeWasm.Trade, 42, 1, 500000000n, 10000n); +assertEqual(evt.symbolId, 42, 'symbolId = 42'); +assertEqual(evt.venueId, 1, 'venueId = 1'); +assertEqual(evt.priceFp, 500000000n, 'priceFp = 500000000n'); +assertEqual(evt.side, undefined, 'side initially undefined'); +evt.side = SideWasm.Ask; +assertEqual(evt.side, SideWasm.Ask, 'side after set = Ask'); +const evtJson = evt.toJson(); +assert(evtJson !== null && evtJson !== undefined, 'toJson() returns non-null'); +const evt2 = MarketEventWasm.fromJson(evtJson); +assertEqual(evt2.symbolId, 42, 'fromJson round-trip preserves symbolId'); +evt.free(); +evt2.free(); + +console.log('\n--- GateConfig ---'); +const config = new GateConfigWasm(); +assertEqual(config.mincutFloorCalm, 12n, 'default mincutFloorCalm = 12n'); +assertEqual(config.mincutFloorNormal, 9n, 'default mincutFloorNormal = 9n'); +assertEqual(config.mincutFloorVolatile, 6n, 'default mincutFloorVolatile = 6n'); +config.cusumThreshold = 5.0; +assertEqual(config.cusumThreshold, 5.0, 'cusumThreshold setter works'); + +console.log('\n--- Coherence Gate ---'); +const gate = new ThresholdGateWasm(config); +const ctx = new GateContextWasm( + 42, 1, 1000000n, 15n, + '00000000000000000000000000000000', + 1.0, 0.1, + RegimeLabelWasm.Calm, + 10, +); +assertEqual(ctx.symbolId, 42, 'context symbolId = 42'); +assertEqual(ctx.regime, RegimeLabelWasm.Calm, 'context regime = Calm'); +assertEqual(ctx.partitionHash, '00000000000000000000000000000000', 'context partitionHash'); + +const decision = gate.evaluate(ctx); +assertEqual(decision.allowRetrieve, true, 'decision allowRetrieve'); +assertEqual(decision.allowWrite, true, 'decision allowWrite'); +assertEqual(decision.allowLearn, true, 'decision allowLearn'); +assertEqual(decision.allowAct, true, 'decision allowAct'); +assertEqual(decision.allAllowed(), true, 'decision allAllowed()'); +assertEqual(decision.fullyBlocked(), false, 'decision fullyBlocked()'); +assert(typeof decision.partitionHash === 'string' && decision.partitionHash.length === 32, + 'decision partitionHash is 32-char hex'); +const reasons = decision.reasons(); +assert(Array.isArray(reasons), 'reasons() returns array'); +assertEqual(reasons.length, 0, 'no reasons when all allowed'); + +console.log('\n--- Coherence Gate (blocked) ---'); +const ctx2 = new GateContextWasm( + 42, 1, 1000000n, 3n, + '00000000000000000000000000000000', + 5.0, 0.8, + RegimeLabelWasm.Calm, + 2, +); +const d2 = gate.evaluate(ctx2); +assertEqual(d2.allowAct, false, 'blocked: allowAct = false'); +assertEqual(d2.allowWrite, false, 'blocked: allowWrite = false'); +assertEqual(d2.allowLearn, false, 'blocked: allowLearn = false'); +assert(d2.reasons().length > 0, 'blocked: has reasons'); + +console.log('\n--- GraphDelta ---'); +const delta = new GraphDeltaWasm(); +const nodes = delta.nodesAdded(); +assert(Array.isArray(nodes), 'nodesAdded returns array'); +assertEqual(nodes.length, 0, 'empty delta has no nodes'); +delta.free(); + +console.log('\n--- ReservoirStore ---'); +const store = new ReservoirStoreWasm(100); +assertEqual(store.isEmpty(), true, 'new store is empty'); +assertEqual(store.len(), 0, 'new store len = 0'); + +// Retrieve from empty store +const empty = store.retrieveBySymbol(42, 10); +assert(Array.isArray(empty), 'retrieveBySymbol returns array'); +assertEqual(empty.length, 0, 'empty store returns 0 results'); +store.free(); + +// Zero-size rejected +let threw = false; +try { new ReservoirStoreWasm(0); } catch (e) { threw = true; } +assert(threw, 'ReservoirStoreWasm(0) throws'); + +// ---------- Summary ---------- +console.log(`\n${'='.repeat(40)}`); +console.log(`Results: ${pass} passed, ${fail} failed`); +console.log(`${'='.repeat(40)}\n`); + +if (fail > 0) process.exit(1); diff --git a/docs/adr/ADR-085-neural-trader-ruvector.md b/docs/adr/ADR-085-neural-trader-ruvector.md new file mode 100644 index 000000000..6894bb92c --- /dev/null +++ b/docs/adr/ADR-085-neural-trader-ruvector.md @@ -0,0 +1,974 @@ +# ADR-085: RuVector Neural Trader β€” Dynamic Market Graphs, MinCut Coherence Gating, and Proof-Gated Mutation + +## Status + +Proposed + +## Date + +2026-03-06 + +## Deciders + +ruv + +## Related + +- ADR-016 RuVector integration patterns +- ADR-030 RVF computational container +- ADR-040 WASM programmable sensing +- ADR-041 curated module registry +- ADR-042 Security RVF AIDefence TEE +- ADR-047 Proof-gated mutation protocol +- `examples/neural-trader/` existing example scaffold +- Cognitive MinCut Engine +- Mincut Gated Transformer +- ruvector-postgres architecture +- Cognitum Gate coherence layer + +## Context + +Most trading systems still split the problem the wrong way. + +They keep market data in one system, features in another, models in another, audit logs somewhere else, and risk logic in handwritten code wrapped around the outside. That creates latency, drift, and a complete mess when you try to explain why a model made a decision or why a learning update happened. + +A neural trader built on RuVector should treat the market as a living graph, not a table of candles. The limit order book, executions, cancellations, venue changes, and cross-asset interactions form a dynamic relational structure. That structure is exactly where short-horizon edge exists. + +The design goal is a single substrate where: + +1. Raw market events become typed graph state +2. Vector embeddings represent evolving microstructure state +3. GNN and temporal attention operate directly on that state +4. Dynamic mincut acts as a first-class coherence and fragility signal +5. Every state mutation and policy action is proof-gated and attestable +6. Online learning remains bounded, replayable, and auditable + +This ADR defines the RuVector-native implementation for Neural Trader as a coherence-first trading substrate for prediction, risk control, and bounded execution research. + +## Decision + +We will implement Neural Trader as a RuVector-native market intelligence stack with six layers: + +1. Ingest and normalization layer +2. Dynamic heterogeneous market graph in RuVector +3. Vector and graph learning layer using temporal GNN and attention +4. Two-stage memory selection and replay layer +5. MinCut-based coherence gate for write, retrieve, learn, and act +6. Policy and actuation layer with proof-gated mutation and witness logs + +The system will use Postgres as the relational source of record, with `ruvector-postgres` as the embedded vector engine and the RuVector graph substrate for dynamic structural reasoning. + +**No model output may directly mutate live strategy state, place orders, or update memory without passing coherence, risk, and policy gates.** + +## Why This Decision + +This approach matches the actual shape of markets. + +A limit order book is not just a time series. It is a dynamic graph with queue locality, price adjacency, event causality, hidden liquidity hints, and regime-dependent cross-symbol coupling. A graph-plus-vector substrate captures that directly. + +RuVector also gives us something most trading systems do not have: + +1. Dynamic mincut as a real-time structural integrity signal +2. Unified vector-plus-graph storage +3. Replayable witness logs +4. Proof-gated state mutation +5. Local-first deployment paths from server to WASM to edge nodes + +The result is a trading research platform that optimizes for **bounded intelligence** rather than blind prediction. + +## Scope + +### In scope + +1. Market data representation +2. RuVector schema +3. Embedding and learning design +4. Memory selection +5. Coherence gating +6. Actuation policy +7. Verification and auditability +8. Deployment topology + +### Out of scope + +1. Broker-specific adapters in detail +2. Exchange colocation engineering +3. Final production capital allocation policy +4. Regulatory filing requirements by jurisdiction + +## Assumptions + +1. Primary use case is short-horizon market making, execution assistance, or micro-alpha research. +2. Input streams include order book updates, trades, cancels, modifies, venue metadata, and optional cross-asset feeds. +3. Latency budget is sub-second for research serving, with optional lower-latency kernels for action gating. +4. Hidden liquidity cannot be observed directly, so proxies are inferred from event patterns. +5. Online learning must remain bounded and reversible. +6. Correctness is treated as adversarially stressed rather than guaranteed. + +--- + +## Architecture + +### 1. Ingest and Normalization + +**Input streams:** + +1. L2 or L3 order book deltas +2. Trades and fills +3. Order lifecycle events (new, modify, cancel, expire) +4. Venue state and session markers +5. Symbol metadata +6. Optional news, macro, or derived volatility streams + +**Normalization output:** + +1. Canonical event envelopes +2. Sequence-aligned timestamps +3. Symbol and venue partition keys +4. Side, price, size, aggressor, queue, and microstructure features +5. Compact hashes for traceability + +**Canonical event envelope:** + +```rust +pub struct MarketEvent { + pub event_id: [u8; 16], + pub ts_exchange_ns: u64, + pub ts_ingest_ns: u64, + pub venue_id: u16, + pub symbol_id: u32, + pub event_type: EventType, + pub side: Option, + pub price_fp: i64, + pub qty_fp: i64, + pub order_id_hash: Option<[u8; 16]>, + pub participant_id_hash: Option<[u8; 16]>, + pub flags: u32, + pub seq: u64, +} +``` + +### 2. RuVector Graph Model + +The order book becomes a typed heterogeneous dynamic graph. + +**Node kinds:** + +| # | Kind | Description | +|---|------|-------------| +| 1 | Symbol | Tradable instrument | +| 2 | Venue | Exchange or dark pool | +| 3 | PriceLevel | Individual price level in the book | +| 4 | Order | Resting or aggressing order proxy | +| 5 | Trade | Matched execution | +| 6 | Event | Raw market event | +| 7 | Participant | Anonymized participant proxy | +| 8 | TimeBucket | Discretized time window | +| 9 | Regime | Market regime classification | +| 10 | StrategyState | Current strategy context | + +**Edge kinds:** + +| # | Edge | From β†’ To | +|---|------|-----------| +| 1 | `AT_LEVEL` | Order β†’ PriceLevel | +| 2 | `NEXT_TICK` | PriceLevel ↔ PriceLevel | +| 3 | `GENERATED` | Event β†’ Order or Trade | +| 4 | `MATCHED` | Aggressor ↔ Resting order proxy | +| 5 | `MODIFIED_FROM` | Order β†’ Order (prior version) | +| 6 | `CANCELED_BY` | Event β†’ Order | +| 7 | `BELONGS_TO_SYMBOL` | * β†’ Symbol | +| 8 | `ON_VENUE` | * β†’ Venue | +| 9 | `IN_WINDOW` | * β†’ TimeBucket | +| 10 | `CORRELATED_WITH` | Symbol ↔ Symbol | +| 11 | `IN_REGIME` | TimeBucket β†’ Regime | +| 12 | `AFFECTS_STATE` | * β†’ StrategyState | + +**Core properties β€” PriceLevel:** + +- Visible depth +- Estimated hidden depth +- Queue length +- Local imbalance +- Refill rate +- Depletion rate +- Spread distance +- Local realized volatility + +**Core properties β€” Order:** + +- Side +- Limit price +- Current queue estimate +- Age +- Modify count +- Cancel hazard score +- Fill hazard score + +**Core properties β€” Trade:** + +- Aggressor side +- Size +- Slippage to mid +- Post-trade impact window + +**Core properties β€” Edge:** + +- Event time delta +- Transition count +- Influence score +- Coherence contribution +- Venue confidence + +### 3. Vector Representation + +Each important subgraph window is embedded into RuVector. + +**Embedding families:** + +1. Book state embedding +2. Queue state embedding +3. Event stream embedding +4. Cross-symbol regime embedding +5. Strategy context embedding +6. Risk context embedding + +**Recommended representation split:** + +1. Dense float embeddings for state similarity +2. Compressed low-bit serving vectors for fast retrieval +3. Graph neighborhood fingerprints for structural similarity +4. Contrastive delta embeddings for regime shift detection + +**Example keyspaces in ruvector-postgres:** + +```sql +-- Event log: range-partitioned by ts_exchange_ns for bounded retention +CREATE TABLE nt_event_log ( + event_id BYTEA NOT NULL, + ts_exchange_ns BIGINT NOT NULL, + ts_ingest_ns BIGINT NOT NULL, + venue_id INT NOT NULL, + symbol_id INT NOT NULL, + event_type INT NOT NULL, + payload JSONB NOT NULL, + witness_hash BYTEA, + PRIMARY KEY (ts_exchange_ns, event_id) +) PARTITION BY RANGE (ts_exchange_ns); + +CREATE INDEX idx_event_log_symbol_ts + ON nt_event_log (symbol_id, ts_exchange_ns); +CREATE INDEX idx_event_log_venue_ts + ON nt_event_log (venue_id, ts_exchange_ns); + +-- Embeddings: composite index for time-range similarity queries +CREATE TABLE nt_embeddings ( + embedding_id BIGSERIAL PRIMARY KEY, + symbol_id INT NOT NULL, + venue_id INT NOT NULL, + ts_ns BIGINT NOT NULL, + embedding_type TEXT NOT NULL, + dim INT NOT NULL, + metadata JSONB NOT NULL, + embedding vector(256) +); + +CREATE INDEX idx_embeddings_symbol_ts + ON nt_embeddings (symbol_id, ts_ns DESC); +CREATE INDEX idx_embeddings_type_ts + ON nt_embeddings (embedding_type, ts_ns DESC); +CREATE INDEX idx_embeddings_vec_hnsw + ON nt_embeddings USING hnsw (embedding vector_cosine_ops) + WITH (m = 16, ef_construction = 200); + +-- Replay segments: partitioned by start_ts_ns for retention management +CREATE TABLE nt_segments ( + segment_id BIGSERIAL NOT NULL, + symbol_id INT NOT NULL, + start_ts_ns BIGINT NOT NULL, + end_ts_ns BIGINT NOT NULL, + segment_kind TEXT NOT NULL, + rvf_blob BYTEA, + signature BYTEA, + witness_hash BYTEA, + metadata JSONB, + PRIMARY KEY (start_ts_ns, segment_id) +) PARTITION BY RANGE (start_ts_ns); + +CREATE INDEX idx_segments_symbol_ts + ON nt_segments (symbol_id, start_ts_ns DESC); +CREATE INDEX idx_segments_kind + ON nt_segments (segment_kind, start_ts_ns DESC); +``` + +### 4. Learning Layer + +We will use a temporal graph learning stack. + +**Model family:** + +1. Typed message passing over dynamic graph neighborhoods +2. Temporal attention over recent event windows +3. Optional sequence head for action or risk outputs +4. Auxiliary contrastive loss for regime separation +5. Coherence regularization using mincut and boundary stability + +**Primary prediction heads:** + +1. Next-window mid-price move probability +2. Fill probability for candidate placements +3. Cancel probability for resting liquidity +4. Slippage risk +5. Local volatility jump risk +6. Regime transition probability + +**Control heads:** + +1. Place or do-not-place +2. Modify or hold +3. Size scaling factor +4. Venue selection +5. Learning write admission score + +**Loss design β€” total loss:** + +``` +L = L_pred + λ₁·L_fill + Ξ»β‚‚Β·L_risk + λ₃·L_contrast + Ξ»β‚„Β·L_coherence + Ξ»β‚…Β·L_budget +``` + +Where: + +- `L_pred` β€” predicts short-horizon outcome +- `L_fill` β€” estimates execution quality +- `L_risk` β€” penalizes unstable high-drawdown actions +- `L_contrast` β€” separates regimes and recurrent motifs +- `L_coherence` β€” penalizes representation drift across stable partitions +- `L_budget` β€” penalizes actions that exceed risk or actuation budgets + +### 5. Memory Design + +Memory must be selective, bounded, and useful. + +#### Stage A: Streaming Sketch + +Keep cheap summaries for recent heavy hitters. + +**Structures:** + +1. Count-Min sketch for repeated motifs +2. Top-K for impactful levels, venues, regimes +3. Rolling range sketches for volatility and imbalance bands +4. Delta histograms for event transitions + +**Purpose:** + +1. Detect recurring market motifs +2. Prioritize candidate memory writes +3. Reduce storage pressure +4. Preserve streaming summaries even when raw fragments age out + +#### Stage B: Uncertainty-Guided Reservoir + +Store high-value replay fragments when one or more conditions hold: + +1. High model uncertainty +2. Large realized PnL impact +3. Regime transition +4. Structural anomaly +5. Rare queue pattern +6. High disagreement between model heads + +Each stored fragment becomes an RVF or signed segment containing: + +1. Compact subgraph +2. Embeddings +3. Labels and realized outcomes +4. Coherence statistics +5. Lineage metadata +6. Witness hash and signature + +### 6. Coherence Gate + +Dynamic mincut is the central gate. + +We compute a compact induced subgraph linking: + +1. Incoming market events +2. Local price levels +3. Relevant prior memories +4. Current strategy state +5. Risk nodes + +From this graph we derive: + +1. Canonical mincut partition +2. Cut value +3. Boundary node identities +4. Cut drift over time +5. Embedding drift by partition +6. CUSUM alarms over cut metrics + +**Gate uses:** + +1. Memory write admission +2. Memory retrieval confidence +3. Online learner update permission +4. Action permission +5. Early rollback trigger +6. Anomaly escalation + +**Gate policy β€” allow only when ALL are true:** + +1. Cut value above floor for current regime +2. Boundary identity stable across last N windows +3. No sustained CUSUM breach +4. Risk budgets available +5. Policy allows actuation +6. Model confidence exceeds threshold conditioned on coherence + +**Gate result type:** + +```rust +pub struct CoherenceDecision { + pub allow_retrieve: bool, + pub allow_write: bool, + pub allow_learn: bool, + pub allow_act: bool, + pub mincut_value: u64, + pub partition_hash: [u8; 16], + pub drift_score: f32, + pub cusum_score: f32, + pub reasons: Vec, +} +``` + +--- + +## Proof-Gated Mutation + +No state mutation occurs without a proof token. + +This includes: + +1. Memory writes +2. Model promotion +3. Policy threshold changes +4. Live order intents +5. Strategy state transitions + +**Mutation protocol:** + +1. Compute features and local graph +2. Compute coherence decision +3. Evaluate policy kernel +4. Mint verified token if allowed +5. Apply mutation +6. Append witness receipt + +**Receipt fields:** + +1. Timestamp +2. Model ID +3. Input segment hash +4. Coherence witness hash +5. Policy hash +6. Action intent +7. Verified token ID +8. Resulting state hash + +--- + +## Serving Flow + +### Research or Paper Trading Path + +1. Ingest market events +2. Update graph and embeddings +3. Retrieve similar memory fragments +4. Compute model outputs +5. Run coherence gate +6. Run policy and budget checks +7. Emit action recommendation +8. Store replay artifacts if admitted + +### Live Bounded Execution Path + +1. Ingest event burst +2. Update local graph cache +3. Score candidate actions +4. Compute mincut coherence +5. Check exposure and slippage budgets +6. Require proof token +7. Publish broker intent +8. Record signed receipt + +--- + +## Policy Kernel + +The policy kernel is explicit and auditable. + +**Inputs:** + +1. Coherence decision +2. Model outputs +3. Position state +4. Exposure limits +5. Venue constraints +6. Liquidity conditions +7. Market halts or macro blocks + +**Rules:** + +1. Never place if coherence is unstable +2. Never upsize in regime uncertainty spike +3. Never write memory during adversarial drift burst unless explicitly quarantined +4. Never learn online when realized slippage exceeds bound and cut drift is rising +5. Always throttle actuation when order rate or cancel rate limits approach venue thresholds + +--- + +## Data Retention and Lineage + +### Three Tiers + +**Hot tier:** + +- Recent event graph state +- Recent embeddings +- Recent witness chain +- Active memory reservoir + +**Warm tier:** + +- Signed replay segments +- Compressed embeddings +- Model evaluation sets +- Daily partition statistics + +**Cold tier:** + +- Long-horizon archives +- Training corpora +- Promoted model lineage +- Audit snapshots + +**Lineage requirements:** + +1. Every model maps to training fragments +2. Every live action maps to model and policy version +3. Every mutation maps to a verified token and witness chain +4. Every rollback maps to explicit trigger and prior state hash + +--- + +## RuVector Implementation Details + +### Collections + +Recommended logical collections: + +1. `nt_market_graph` +2. `nt_embeddings_hot` +3. `nt_embeddings_archive` +4. `nt_memory_segments` +5. `nt_policy_receipts` +6. `nt_model_registry` +7. `nt_regime_index` + +### Indexing + +1. HNSW or RuVector ANN for embedding retrieval +2. Graph neighborhood cache for local subgraph extraction +3. Time-partitioned relational tables in Postgres +4. Quantized serving vectors for low-latency retrieval +5. Optional hyperbolic geometry for regime and hierarchy embeddings + +### Retrieval Strategy + +Hybrid retrieval score: + +``` +S = Ξ±Β·similarity + Ξ²Β·structural_overlap + Ξ³Β·regime_match + δ·coherence_bonus +``` + +Where: + +- `similarity` β€” vector distance +- `structural_overlap` β€” graph neighborhood match +- `regime_match` β€” volatility and spread regime comparison +- `coherence_bonus` β€” reward for fragments from stable partitions + +Weights are constrained: `Ξ± + Ξ² + Ξ³ + Ξ΄ = 1`. Defaults: `Ξ±=0.4, Ξ²=0.25, Ξ³=0.2, Ξ΄=0.15`. Tuned per regime via walk-forward validation. + +--- + +## Rust Module Layout + +``` +crates/ + neural-trader-core/ # Event schema, types, ingest + neural-trader-graph/ # Dynamic heterogeneous market graph + neural-trader-features/ # Feature extraction and embedding + neural-trader-memory/ # Two-stage memory selection + neural-trader-coherence/ # MinCut coherence gate + neural-trader-policy/ # Policy kernel and risk budgets + neural-trader-execution/ # Broker adapters, order intent + neural-trader-replay/ # RVF replay segments, witness logs + neural-trader-rvf/ # RVF serialization bindings + neural-trader-server/ # gRPC/HTTP serving layer +``` + +### Core Traits + +```rust +pub trait EventIngestor { + fn ingest(&mut self, event: MarketEvent) -> anyhow::Result<()>; +} + +pub trait GraphUpdater { + fn apply_event(&mut self, event: &MarketEvent) -> anyhow::Result; +} + +pub trait Embedder { + fn embed_state(&self, ctx: &StateWindow) -> anyhow::Result>; +} + +pub trait MemoryStore { + fn retrieve(&self, query: &MemoryQuery) -> anyhow::Result>; + fn maybe_write( + &mut self, + seg: MemorySegment, + gate: &CoherenceDecision, + ) -> anyhow::Result; +} + +pub trait CoherenceGate { + fn evaluate(&self, ctx: &GateContext) -> anyhow::Result; +} + +pub trait PolicyKernel { + fn decide(&self, input: &PolicyInput) -> anyhow::Result; +} + +pub trait WitnessLogger { + fn append_receipt(&mut self, receipt: WitnessReceipt) -> anyhow::Result<()>; +} +``` + +--- + +## Training Plan + +### Offline Phase + +1. Ingest historical L2 or L3 streams +2. Build dynamic graph windows +3. Create replay segments +4. Train temporal GNN and retrieval heads +5. Calibrate confidence +6. Validate on walk-forward splits +7. Measure coherence-aware versus non-coherence baselines + +### Online Bounded Adaptation + +**Allowed:** + +1. Calibration updates +2. Retrieval weighting +3. Memory admission thresholds +4. Narrow regime adaptation + +**Forbidden without manual promotion:** + +1. Major architecture changes +2. Policy kernel changes +3. Risk budget changes +4. Output head rewiring + +--- + +## Evaluation + +### Core Metrics + +**Prediction:** + +1. Fill probability calibration +2. Short-horizon direction AUC +3. Slippage error +4. Realized adverse selection + +**Trading:** + +1. PnL +2. Sharpe or information ratio +3. Max drawdown +4. Inventory risk +5. Cancel-to-fill quality +6. Venue quality + +**Coherence:** + +1. Average mincut by regime +2. Partition stability +3. Drift detection precision +4. False-positive gate rate +5. Rollback trigger quality + +**Systems:** + +1. p50 / p95 / p99 latency +2. Retrieval latency +3. Write amplification +4. Storage growth +5. Witness overhead + +--- + +## Acceptance Criteria + +### Phase 1 β€” Research + +1. Replayable end-to-end pipeline +2. Deterministic witness logs +3. Measurable improvement from graph-plus-coherence over price-only baseline +4. Bounded online updates with rollback + +### Phase 2 β€” Paper Trading + +1. Stable gate behavior under live feed noise +2. No uncontrolled action bursts +3. No unverified mutations +4. Explainable receipts for every recommendation + +### Phase 3 β€” Live Small Capital + +1. Strict exposure limits enforced +2. Slippage within approved band +3. Rollback tested in production shadow mode +4. Daily audit completeness at 100% + +--- + +## Safety and Governance + +### Mandatory Controls + +1. Notional exposure caps +2. Per-symbol limits +3. Sector or cross-asset correlation caps +4. Order rate and cancel rate caps +5. Slippage budget +6. Venue health checks +7. Market halt awareness +8. Human override and kill switch + +### Governance Requirements + +1. All policy changes versioned +2. All model promotions signed +3. All live mutations proof-gated +4. All replay sets immutable after seal +5. All exceptions logged with witness chain + +--- + +## Failure Modes + +### 1. Regime Shift Masquerading as Edge + +**Symptom:** Model confidence rises while execution deteriorates. + +**Fix:** Increase weight of coherence gate, reduce online learning scope, quarantine new memory writes. + +### 2. Retrieval Poisoning + +**Symptom:** Bad fragments dominate replay or inference retrieval. + +**Fix:** Signed segment lineage, structural overlap thresholding, memory deprecation, reservoir diversity constraints. + +### 3. Feedback Loop with Market Impact + +**Symptom:** Strategy reacts to its own footprint. + +**Fix:** Actuation throttles, self-impact features, venue split, delayed reinforcement of impacted samples. + +### 4. Overfitting to Stable Partitions + +**Symptom:** System ignores true novelty. + +**Fix:** Maintain novelty quota in memory reservoir, adversarial validation, regime-balanced evaluation. + +### 5. Latency Creep + +**Symptom:** Graph growth degrades serving time. + +**Fix:** Compact local subgraphs, quantized embeddings, hot-path kernels, bounded neighborhood extraction. + +--- + +## Alternatives Considered + +### Alternative A: Pure Time-Series Transformer + +Over candles and book tensors. + +**Rejected:** Ignores explicit queue topology, event causality, and structural integrity. + +### Alternative B: Traditional Feature Engineering + Boosted Trees + +**Rejected:** Works in narrow slices, but memory, structure, and drift handling remain bolted on rather than native. + +### Alternative C: End-to-End RL Trader + +**Rejected:** Action-space instability, reward hacking risk, and poor auditability for early deployment. + +--- + +## Consequences + +### Positive + +1. Unified substrate for data, memory, learning, and governance +2. Explicit structural reasoning over market microstructure +3. Bounded and auditable online learning +4. First-class drift and fragility detection +5. Reproducible replays and mutation receipts + +### Negative + +1. More complex graph engineering +2. Higher initial systems effort than plain tensor pipelines +3. Policy design must be disciplined +4. Coherence thresholds require calibration by regime + +--- + +## Implementation Plan + +### Phase 1 β€” Foundation + +1. Define canonical market event schema +2. Implement RuVector graph projection +3. Implement hot embedding pipeline +4. Implement replay segment writer +5. Implement mincut gate service +6. Implement witness receipts + +### Phase 2 β€” Learning + +1. Train baseline GNN plus temporal attention +2. Add retrieval-augmented prediction +3. Add uncertainty scoring +4. Add reservoir memory writer +5. Compare against price-only baseline + +### Phase 3 β€” Bounded Action + +1. Implement policy kernel +2. Implement paper trading adapter +3. Add risk budgets and throttles +4. Test rollback +5. Certify live shadow mode + +### Phase 4 β€” Live Research + +1. Small capital deployment +2. Conservative venue set +3. Daily audit review +4. Promote only signed models +5. Continuous regime monitoring + +--- + +## Minimal Example Configuration + +```yaml +neural_trader: + symbol_universe: + - ES + - NQ + - CL + + ingest: + venue_clock_tolerance_ns: 500000 + reorder_buffer_events: 2048 + + graph: + max_local_levels_per_side: 32 + max_orders_per_window: 5000 + neighborhood_hops: 2 + + embeddings: + dim: 256 + quantized_dim: 256 + similarity_metric: cosine + + memory: + stage_a: + count_min_width: 4096 + count_min_depth: 4 + topk: 256 + stage_b: + reservoir_size: 50000 + min_uncertainty: 0.18 + min_realized_impact_bp: 1.5 + + coherence: + mincut_floor_by_regime: + calm: 12 + normal: 9 + volatile: 6 + cusum_threshold: 4.5 + boundary_stability_windows: 8 + + policy: + max_notional_usd: 250000 + max_symbol_notional_usd: 50000 + max_order_rate_per_sec: 10 + max_cancel_rate_per_sec: 15 + max_slippage_bp: 2.0 + require_verified_token: true + + learning: + online_mode: bounded + allow_calibration_updates: true + allow_memory_write: true + allow_weight_updates: false + + retention: + hot_window_hours: 4 + warm_retention_days: 30 + cold_archive_days: 365 + partition_interval_ns: 3600000000000 # 1 hour per partition + vacuum_schedule_cron: "0 */6 * * *" +``` + +--- + +## Decision Summary + +Neural Trader will be built as a RuVector-native dynamic market graph system where vectors, graphs, temporal learning, and dynamic mincut work together as one bounded intelligence loop. + +The core principle is simple: + +> **Do not trust prediction alone. Trust prediction only when the surrounding market structure is coherent enough to justify learning, remembering, or acting.** + +That gives us a trader that is not just neural, but **structurally self-aware**. + +### Implementation Priority + +Best immediate path is three crates first: + +1. **`neural-trader-core`** β€” ingest, canonical types, event schema +2. **`neural-trader-coherence`** β€” mincut gating, coherence decisions +3. **`neural-trader-replay`** β€” witnessable segments, RVF integration + +That gets ingest, witnessable segments, and mincut gating working before the full model stack is finalized. + +**Stretch option:** Adding a Mincut Gated Transformer head for early exit and sparse compute during regime instability. + +**Frontier option:** Deploying the coherence gate as a tiny deterministic kernel on Cognitum-style edge nodes or WASM workers so action permission stays cheap, bounded, and independently attestable. + +**Benchmark test:** On replay, the coherence-gated model should beat a tensor-only baseline on slippage-adjusted PnL while reducing unstable memory writes and false actuation during regime shifts. diff --git a/docs/adr/ADR-086-neural-trader-wasm.md b/docs/adr/ADR-086-neural-trader-wasm.md new file mode 100644 index 000000000..97774b53d --- /dev/null +++ b/docs/adr/ADR-086-neural-trader-wasm.md @@ -0,0 +1,97 @@ +# ADR-086: Neural Trader WASM Bindings + +## Status + +Accepted + +## Date + +2026-03-06 + +## Deciders + +ruv + +## Related + +- ADR-085 Neural Trader architecture (core, coherence, replay crates) +- ADR-084 ruvllm-wasm publish & Rust 1.91 WASM codegen workaround +- ADR-040 WASM programmable sensing +- ADR-041 curated module registry + +## Context + +The three Neural Trader Rust crates (`neural-trader-core`, `neural-trader-coherence`, `neural-trader-replay`) provide a coherence-gated market graph system with 12 passing tests. However, they have no browser/WASM bindings β€” meaning dashboards, backtesting UIs, and browser-based research tools cannot access the coherence gate or replay memory directly. + +The repository has an established WASM crate pattern used by 15+ crates (`ruvector-wasm`, `ruvector-gnn-wasm`, `ruvector-attention-wasm`, `ruvllm-wasm`, etc.) all using `wasm-bindgen` + `serde-wasm-bindgen` + `wasm-pack`. We follow that pattern here. + +### Rust 1.91 WASM Codegen Bug + +Rust 1.91 has a known WASM codegen bug in release mode. The workaround (same as `ruvllm-wasm`, documented in ADR-084) is: + +```bash +CARGO_PROFILE_RELEASE_CODEGEN_UNITS=256 CARGO_PROFILE_RELEASE_LTO=off \ + wasm-pack build --target web --scope ruvector --release +``` + +Additionally, `wasm-opt` is disabled via `[package.metadata.wasm-pack.profile.release] wasm-opt = false`. + +## Decision + +Create `crates/neural-trader-wasm/` with a single `src/lib.rs` that wraps all three crates using the `XxxWasm { inner: Xxx }` pattern. + +### Type Mapping Strategy + +| Rust Type | WASM Approach | +|-----------|--------------| +| C-style enums (`EventType`, `Side`, `RegimeLabel`, etc.) | Mirror as `#[wasm_bindgen]` enums with `From` conversions | +| `[u8; 16]` fields (event IDs, hashes) | Hex strings in JS (32-char), decoded at boundary | +| Simple structs (`GateConfig`) | Direct field getters/setters via `#[wasm_bindgen(getter/setter)]` | +| Complex nested structs (`MarketEvent`, `ReplaySegment`) | `toJson()`/`fromJson()` via `serde-wasm-bindgen` | +| `Vec` and `VecDeque` | Serialized to `JsValue` via `serde-wasm-bindgen` | +| `anyhow::Result` | Converted to `Result` at WASM boundary | +| Trait objects (`CoherenceGate`) | Concrete `ThresholdGateWasm` wrapper (no dyn dispatch in WASM) | + +### Exported Types + +| Source Crate | WASM Type | Key Methods | +|-------------|-----------|-------------| +| core | `MarketEventWasm` | `new()`, field getters/setters, `toJson()`, `fromJson()` | +| core | `GraphDeltaWasm` | `new()`, `nodesAdded()`, `edgesAdded()`, `propertiesUpdated()` | +| core | `EventTypeWasm`, `SideWasm`, `NodeKindWasm`, `EdgeKindWasm` | C-style enums | +| coherence | `GateConfigWasm` | `new()` with defaults, all threshold getters/setters | +| coherence | `ThresholdGateWasm` | `new(config)`, `evaluate(ctx)` | +| coherence | `GateContextWasm` | `new(...)`, field getters | +| coherence | `CoherenceDecisionWasm` | `allowRetrieve`, `allowWrite`, `allowLearn`, `allowAct`, `reasons()`, `toJson()` | +| coherence | `RegimeLabelWasm` | C-style enum | +| replay | `ReservoirStoreWasm` | `new(maxSize)`, `len()`, `isEmpty()`, `maybeWrite(segJson, decisionJson)` | +| replay | `ReplaySegmentWasm` | `toJson()`, `fromJson()`, field getters | +| replay | `SegmentKindWasm` | C-style enum | +| β€” | `version()` | Crate version string | +| β€” | `healthCheck()` | Returns `true` | + +### Build & Package + +- Built with `wasm-pack build --target web --scope ruvector --release` +- Published to npm as `@ruvector/neural-trader-wasm` +- `publish = false` in Cargo.toml (Rust crate not on crates.io) + +## Consequences + +### Positive + +- Browser dashboards can evaluate coherence gates without a backend roundtrip +- Research notebooks (Observable, Jupyter) can use the replay memory directly +- TypeScript types auto-generated by wasm-pack from `#[wasm_bindgen]` annotations +- Follows the same pattern as 15+ existing WASM crates in the workspace + +### Negative + +- Trait objects (`CoherenceGate`, `MemoryStore`) cannot be passed across WASM boundary; only concrete implementations are exposed +- `[u8; 16]` hex encoding adds a small overhead at the boundary (negligible vs. WASM call overhead) +- `ReservoirStore.maybeWrite()` takes JSON values rather than typed structs due to WASM ownership constraints + +### Risks + +- Rust 1.91 WASM codegen bug requires env-var workaround; future Rust versions should fix this +- `serde-wasm-bindgen` 0.6 is a newer dependency not yet in the workspace; pinned to avoid surprises