Issues

Discovered problems out of scope for the current change. Each entry has a label, affected files, description, and fix recommendation. Resolved issues are deleted from this file, not archived.

Performance/allocation issues with no behavioural impact are tracked as optimizations (O1, O2, ...), not here.


Active

I26: Sign error in soc_exact.md AS price-diffusion intermediate step

  • Files: docs/src/reference/soc_exact.md (CARA separation and reduced PDE)
  • Problem: The step deriving the inventory penalty writes 1/2 sigma^2 partial_S^2 V = -1/2 gamma^2 sigma^2 q^2 V. With the ansatz V = -exp(-gamma (X + q S + theta)) the correct derivative is partial_S^2 V = +gamma^2 q^2 V, so the term is +1/2 gamma^2 sigma^2 q^2 V; the written sign is reversed. The final reduced PDE (penalty -1/2 gamma sigma^2 q^2) is correct, so the intermediate equality is internally inconsistent with it.
  • Fix: Flip the sign of the intermediate equality to +1/2 gamma^2 sigma^2 q^2 V, or drop the intermediate step and state the reduced penalty directly.

I20: AvellanedaExact::value_function_theta returns a gauge-shifted theta

  • Files: solver/src/numeric/ode.rs (LinearSpectralSolver::solve_tridiagonal), solver/src/analytical/avellaneda/exact.rs (value_function_theta), neural_solver/tests/data/reference.json (the avellaneda_stoikov.theta values), neural_solver/src/neural_solver/models/avellaneda_stoikov.py.
  • Problem: solve_tridiagonal computes exp(M tau) y0 but returns it scaled by exp(-max_exp) for numerical stability. The scaling cancels in the spread ratios (v_q / v_{q+1}), so exact_spreads is correct, but value_function_theta applies ln to the scaled vector, so its returned theta is shifted by -max_exp / kappa. The absolute theta (and hence the value function) is therefore not the physical reduced value, even though the observable spreads are unaffected.
  • Fix: Either multiply the returned vector back by exp(max_exp) in solve_tridiagonal before any ln is taken, or make value_function_theta add max_exp / kappa back, and regenerate reference.json.

I19: Heston finite-horizon BSDE diverges at the a = 140 test point

  • Files: solver/tests/numerical/bsde_vs_fdm/consistency.rs (test_heston_finite_horizon_bsde_matches_fdm_spread_values), solver/src/models/heston.rs

  • Problem: The decoupled BSDE regression diverged at a = 140 (bid = 10.0, ask = -5.0, fitted value ~5.6e4). Two independent root causes were traced:

    1. Reward/forward discretization inconsistency. Heston::optimize computes the fill intensity as lambda = a * exp(-kappa * delta) with no cap (unlike the Hawkes models). When a transient regression gradient drives a spread to its lower clamp (delta = -5), the intensity becomes a * exp(7.5). At a = 140 that is ~2.5e5, and running_reward = (lambda_bid + lambda_ask) / (gamma + kappa) blows up, closing a positive-feedback loop. The forward step clamps p = lambda * dt to [0, 1], but the reward used the unclamped lambda, so the backward step counted fills the forward step cannot produce. Fix: bound the reward rate at 1/dt per fill side in bsde_driver only, matching the forward probability clamp without touching the lambda returned to to_spreads. Capping the intensity itself (e.g. .min(20)) is incorrect: to_spreads inverts delta = -ln(lambda/a)/kappa, so at a = 140 even the correct symmetric lambda = 59 exceeds any absolute cap, and capping would corrupt the recovered spread and the FD path.

    2. Decoupled proxy-fill forward pass. The finite-horizon test ran BsdeSolver::new (decoupled). For a full-value market-making model the inventory dynamics depend on the optimal control, so the decoupled forward pass simulated inventory fills from the frozen base-spread proxy while the backward step recovered a skewed optimal control. This forward/backward inconsistency produced a large anti-symmetric bid/ask skew at q = 0 (bid 1.03, ask 0.13, total still matching FD). The total spread matched FD but each component was off by ~0.45. Fix: the test uses BsdeSolver::coupled (Picard iteration), which re-simulates the forward inventory under the current optimal control via next_step_controlled. With coupled mode the bid/ask components match FD to ~0.005.

    These two fixes are mathematically required, not duct tape: the reward bound restores the discretization-consistent backward driver, and coupled mode restores the forward/backward consistency of a control-dependent state process.

I9: policy_iteration.rs / operator.rs re-export shims

  • Files: solver/src/numeric/policy_iteration.rs, solver/src/numeric/operator.rs
  • Problem: Both are thin pub use super::finite_difference::*; shims from Phase 2. numeric/mod.rs still calls them "backward-compatible."
  • Fix: Update callers to solver::numeric::finite_difference:: and delete the shims, or document them as permanent.

I11: engine::python::market::legacy duplicates simulation + engine

  • File: engine/src/python/market/legacy.rs
  • Problem: Defines PyEngine, PyEngineGbmAs, etc. plus run_many_* functions, all hardcoded to GBM/Heston/Bates + AS. The newer PySimEngine and run_simulation{,_batch,_summary} cover the same ground generically.
  • Fix: Confirm no external consumers, then delete legacy.rs and its registrations. Otherwise mark #[deprecated].

I14: Python vec_env bindings out of sync with VecEnv

  • File: engine/src/python/market/vec_env.rs
  • Problem: The binding imports crate::vec_env::VecEnvProcess (no longer present), sets VecEnvConfig { process } (the field no longer exists; the config now holds Heston parameters directly), and calls VecEnv::state_dim (the method was removed). cargo check -p engine --features python fails for this reason.
  • Fix: Rewrite the binding against the current VecEnv API. Either expose the fixed 6D state width or reintroduce a state_dim accessor, and drop the removed VecEnvProcess/process indirection.

I15: Orphaned convergence_bump.rs references removed solver APIs

  • File: solver/tests/numerical/exact_vs_finite_diff/convergence_bump.rs
  • Problem: The file is not declared in exact_vs_finite_diff/mod.rs, so it is never compiled. It still imports the removed solver::models::traits::{Gradients, Model}, calls PolicyIterationSolver::solve_grid, and calls Operator::compute_gradients.
  • Fix: Either delete the file (it duplicates convergence_base.rs and convergence_market_impact.rs) or port it to the current ControlProblem/solve_grid_control API and wire it into the module.

I16: Market-making BSDE spread magnitude is ~10x the exact solution

  • Files: solver/src/numeric/bsde/solver.rs, solver/src/models/control.rs, solver/src/models/avellaneda.rs (and drift/impact variants)
  • Problem: The BSDE backward step used only running_reward (the reduced Hamiltonian H*) and omitted the reduced generator (the local inventory-risk source -0.5 gamma sigma^2 q^2), so the market-making value had no inventory skew and the total spread collapsed to 2 * base_spread. The configured InitializationMode was also never applied: every forward path started at the exact initial state, giving a rank-one design that could not identify the inventory gradient. The streaming normal-equation regression squared the design condition number and added a one-dimensional value bias (Merton 9.2%, LQ up to 66%). The impact model's forward proxy also omitted the permanent impact shift.
  • Fix: Added a ControlProblem::bsde_driver hook (default running_reward for full value problems; reduced market-making models add the local source), applied InitializationMode to the initial forward cloud (integer lattice for discrete inventory, continuous for diffusion), switched the default regression backend to Householder QR with a minimal ridge floor for rank-deficiency, and included the impact shift in the impact forward proxy.

I18: Finite-horizon LQ BSDE value carries O(dt) discretization bias

  • Files: solver/src/models/lq_regulator.rs, solver/src/numeric/bsde/solver.rs
  • Problem: The LQ value estimate is biased 0.47% at dt=0.005 and 0.30% at dt=0.0025, scaling with dt (Euler-Maruyama discretization error), not with path count. The <= 0.1% high-precision target requires dt <= ~0.001 at 100_000 paths, which is a slow one-time run, not a defect. Documented here so the discretization floor is tracked separately from Monte Carlo noise.
  • Fix: No code change required; record the achieved dt-limited precision in bsde_precision.md and note that the <= 0.1% target for LQ needs a finer time step.
  • Stale reference: the 0.47%/0.30% figures were measured against LqRegulator::exact_value, which before the Riccati time-indexing fix returned the constant P(0) for every remaining horizon instead of the time-varying P(t). Re-measure the bias against the corrected reference before finalizing the bsde_precision.md numbers.

I21: BSDE mixed-derivative stencil is computed but never consumed

  • Files: solver/src/numeric/bsde/solution.rs (state_derivatives), solver/src/models/control.rs (StateDerivatives)
  • Problem: The BSDE state_derivatives centered four-corner stencil populates hessian_full[i][j] for i != j, but no model's optimize reads those off-diagonal entries. The LQ regulator's optimize returns the closed-form -R^-1 B' P(t) x, which is independent of the diffusion matrix C and ignores derivs entirely; the market-making models' optimize reads only the directional fwd/bwd entries. The Heston rho correlation is a reduced drift correction, not a hessian_full cross term. As a result the BSDE mixed derivative is dead output: it is unit-tested against a synthetic bilinear basis but never validated end to end against a closed form through a real solve. The FD mixed stencil is consumed by the LQ generator and is now guarded by cross_term_changes_fd_value.
  • Fix: Add a correlated control problem whose optimizer genuinely depends on the off-diagonal Hessian (for example multiplicative/state-dependent noise, which has no simple Riccati closed form, or a cost coupling that makes the optimal control read hessian_full), then validate FD and BSDE against its exact or manufactured reference.

I23: Quadratic-generator BSDE test blows up from a wide initial range

  • Files: solver/tests/numerical/bsde_solver_validation/analytical_baselines.rs (test_quadratic_generator_produces_finite_values)
  • Problem: QuadraticGeneratorModel's forward step dy = y^2 dt has finite-time blow-up at t = 1/y. The solver is configured with with_initial_range(5.0), which perturbs the initial cloud over a wide interval around y_init = 0.5; paths seeded with y > 2 blow up before the horizon = 0.5 endpoint, so solve_trajectory_control returns a non-finite value and assert!(bsde_value.is_finite()) fails. The test is unseeded, so the failure is flaky.
  • Fix: Narrow the initial range (for example with_initial_range(0.5)) so the whole cloud stays inside the blow-up radius for the horizon, add with_seed(...), or cap the forward state / shorten the horizon.

I24: Hawkes finite-horizon BSDE vs FDM spread test exceeds tolerance

  • Files: solver/tests/numerical/bsde_vs_fdm/consistency.rs (test_hawkes_finite_horizon_bsde_matches_fdm_spread_values)
  • Problem: The bid or ask spread recovered by the BSDE solver differs from the FD reference by more than tol = 0.15. The test uses 30_000 paths, 3 unseeded repeats, a Hermite(3) basis, and no fixed seed, so it is subject to Monte Carlo variance and the same forward/backward bias family documented in I16, I18, and I19. It fails intermittently rather than deterministically.
  • Fix: Seed the solver with with_seed(...) and either raise num_paths / repeats or relax the tolerance after measuring the actual bias. Check whether the Hawkes forward proxy omits a local source (cf. I16) before treating the residual as pure noise.