jgrusewski c7ccf0c301 feat(rl): R7d — PER wired + off-policy DQN with stop-grad on encoder
Closes plan A9 (rebuild plan's "PER wiring" R7 scope second half;
R7c-data shipped the first half last commit). The `ReplayBuffer` in
`src/rl/replay.rs` has sat as dead code since Phase C — this commit
makes it load-bearing per `feedback_always_per` ("PER always enabled;
non-PER paths are dead code").

## Architecture: off-policy Q + on-policy PPO + V + stop-grad encoder

Shared-encoder pattern with the canonical off-policy + shared-encoder
discipline: the Q head trains from PER-sampled past transitions,
PPO + V train on current-step on-policy data, and the encoder receives
gradient signal ONLY from PPO + V (and BCE/aux via the perception
trainer's separate `step_batched` path). Standard pattern in SAC,
R2D2, IMPALA.

Stop-grad is implemented by computing Q's `grad_h_t` (via
`backward_to_w_b_h(sampled_h_t, ...)`) but NOT accumulating it into
`grad_h_t_combined_d` — the encoder backward only sees π + V
contributions. Per `feedback_no_hiding` the discarded buffer is
allocated and written (the kernel API requires the writeback target);
the discard is a deliberate design call documented at the
accumulation site.

## Wiring summary

### Kernel: `dqn_distributional_q_bwd`
* New `loss_per_batch [B]` output. Atom 0 of each block writes the
  per-sample CE loss (non-atomic — single writer per batch).
  `loss_out [1]` continues to atomicAdd the scalar sum for the
  diagnostic total. Build.rs cache bust v30.

### `DqnHead::backward_logits` (Rust wrapper)
* New `loss_per_batch: &mut CudaSlice<f32>` arg. Migrated atomically
  in the same commit per `feedback_no_partial_refactor` — only
  caller is the integrated trainer.

### `IntegratedTrainerConfig`
* New `per_capacity: usize` (default 4096, matches `replay.rs` doc
  ceiling for naive O(N) sampling).
* New `per_seed: u64` (default 0x9E37_79B9_7F4A_7C15).
* `Default` impl added so test fixtures forward-compat via
  `..IntegratedTrainerConfig::default()`. All 5 existing test
  fixtures migrated.

### `IntegratedTrainer`
* New fields: `replay: ReplayBuffer`, `sampled_h_t_d`,
  `sampled_h_tp1_d`, `sampled_actions_d`, `sampled_rewards_d`,
  `sampled_dones_d`, `sampled_next_actions_d`, `td_per_sample_d`.
* New methods: `push_to_replay(b_size)` — DtoH per-batch metadata
  (action/reward/done/log_pi_old) + alloc per-transition
  `CudaSlice<f32>(HIDDEN_DIM)` ×2 + DtoD per-batch slice copies +
  push to `ReplayBuffer`. `sample_and_gather(b_size)` — read
  per_α from ISV[405], call `replay.sample_indices`, gather sampled
  transitions' h_t/h_tp1 device payloads via per-batch DtoD into
  `sampled_h_t_d` / `sampled_h_tp1_d`, HtoD upload action/reward/done.

### `step_with_lobsim` orchestration
After `compute_advantage_return` and BEFORE `step_synthetic`:
  1. DtoH full ISV slice to refresh `isv_host` (so PER reads ISV[405]
     for per_α).
  2. `push_to_replay(b_size)` — push current step's transitions.
  3. `sample_and_gather(b_size)` — return `per_indices` for the
     priority update.
  4. `step_synthetic(snapshots)` — runs π + V on current-step h_t,
     Q on SAMPLED h_t (off-policy).
  5. DtoH `td_per_sample_d` → host; `replay.update_priorities(
     per_indices, td_per_sample_host)`.
  6. Target-net soft update (unchanged from R5).

### `step_synthetic` redirects (Q path → sampled, π/V stay on-policy)
* Q forward: `forward(&self.sampled_h_t_d)` (was `h_t_borrow`).
* New: forward online Q on `&self.sampled_h_tp1_d` → local scratch +
  `argmax_expected_q` → `self.sampled_next_actions_d`. The
  Double-DQN argmax MUST be recomputed each step (online net weights
  drift faster than transitions recycle through replay; storing
  argmax at push time would feed stale-action data into the
  projection).
* `forward_target(&self.sampled_h_tp1_d)` (was `&self.h_tp1_d`).
* `select_action_atoms(..., &self.sampled_next_actions_d, ...)`
  (was `&self.next_actions_d`).
* `project_bellman_target(..., &self.sampled_rewards_d,
  &self.sampled_dones_d, ...)` (was `rewards_d` / `dones_d`).
* `backward_logits(..., &self.sampled_actions_d, ...,
  &mut self.td_per_sample_d, ...)` (added per-sample loss output).
* `backward_to_w_b_h(&self.sampled_h_t_d, ...)` (was `h_t_borrow`).
* Q grad_h_t accumulation REMOVED from Step 10 (stop-grad).

## Test: r7d_per_wiring.rs (gate G6)
Three invariants per `pearl_tests_must_prove_not_lock_observations`:
  1. `replay.len()` grows by exactly `b_size` per `step_with_lobsim`
     call (push semantics).
  2. `sample_indices(b_size, α)` returns vec of length `b_size` on a
     non-empty buffer.
  3. Buffer caps at `per_capacity` (ring-with-random-replacement).
Drives 15 steps with `per_capacity=8`, asserts growth 0→5→8 across
the cap boundary.

## Acceptable host traffic this commit adds
* Per-step DtoH of 4 × b_size scalars (action/reward/done/log_pi_old)
  for PER push metadata.
* Per-step DtoH of b_size floats (td_per_sample_d) for
  update_priorities.
* Per-step HtoD of 3 × b_size scalars (sampled action/reward/done)
  for sampled metadata gather.
* Per-step DtoD of 2 × b_size × HIDDEN_DIM floats (per-batch h_t /
  h_tp1 slices) for PER push + gather.
PER bookkeeping is a control-plane operation by design (host-side
priority/index management); the device-side training hot path
(encoder, Q/π/V forward/backward, Adam) stays GPU-pure. GPU sum-tree
+ device-resident transitions are a Phase R-future optimization
flagged in `replay.rs`'s doc.

## What's NOT in this commit
* Q `loss_per_batch [B]` is now wired through `backward_logits` but
  the DtoH happens inside step_with_lobsim (not inside
  step_synthetic). Earlier R7d sketches considered a separate
  `dqn_offpolicy_step` method; the in-step_synthetic redirect
  approach landed because it touches fewer lines + reuses the
  existing scratch buffer allocations + matches the trainer's
  established λ-weighted multi-head pattern. A future refactor
  could split for clarity.

Local sm_86 smoke gates: `cargo test -p ml-alpha --test
r7d_per_wiring -- --ignored --nocapture` (G6) +
`integrated_trainer_smoke` (end-to-end). Cluster smoke deferred to
R9.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 12:59:42 +02:00

Foxhunt

Production HFT trading system in Rust.

Architecture

The workspace contains 32 crates organized as follows:

Core Libraries (16)

Crate Purpose
trading_engine Order processing, FIX 4.4, IB TWS, SIMD, RDTSC timing
risk VaR, Kelly, circuit breakers, kill switches, compliance
risk-data Risk data types and shared structures
trading-data Trading data types
ml DQN Rainbow, PPO, TFT, Mamba2, ensemble inference
ml-data ML data types and feature definitions
data Market data ingestion and storage
backtesting Replay engine, strategy tester
adaptive-strategy Ensemble execution, microstructure analysis
common Shared types, resilience, error handling
storage S3 and local model storage
model_loader Model serialization and loading
market-data Market data feed handlers
database PostgreSQL access layer (SQLx)
config Configuration management
tli CLI commands and tooling

Services (8)

Service Purpose
backtesting_service gRPC backtesting service
broker_gateway_service FIX routing, broker connectivity
trading_service Core trading operations
ml_training_service Model training orchestration
data_acquisition_service Market data acquisition
trading_agent_service Autonomous trading agents
api_gateway gRPC API gateway with auth
web-gateway Axum REST + WebSocket gateway

Frontend

web-dashboard/ -- React 19 + TypeScript + Vite + TradingView charts.

Building

# Check compilation (no PostgreSQL required)
SQLX_OFFLINE=true cargo check --workspace

# Run tests for a specific crate
SQLX_OFFLINE=true cargo test -p <crate> --lib

# Clippy
SQLX_OFFLINE=true cargo clippy --workspace

ML Models

Four production model architectures on Candle v0.9.1 with CUDA:

  • DQN Rainbow -- Deep Q-Network with prioritized replay, dueling heads, noisy nets
  • PPO -- Proximal Policy Optimization with GAE and LSTM policies
  • TFT -- Temporal Fusion Transformer for multi-horizon forecasting
  • Mamba2 -- State space model for sequence prediction

Each model has a standalone trainer and a UnifiedTrainable adapter for the hyperopt pipeline.

Infrastructure

  • Git: Gitea at git.fxhnt.ai (Tailscale-only), Scaleway DEV1-S
  • Observability: OpenTelemetry OTLP (env OTEL_EXPORTER_OTLP_ENDPOINT)
  • Database: PostgreSQL with SQLx offline mode for CI

License

Proprietary. All rights reserved.

Description
No description provided
Readme 849 MiB
Languages
Rust 88.2%
Cuda 7.7%
Python 1.3%
Shell 1.1%
PLpgSQL 0.8%
Other 0.8%