solver/models/traits.rs
1// src/models/traits.rs
2
3#[derive(Clone, Copy, Debug)]
4pub struct Gradients<const N: usize> {
5 pub fwd: [f64; N],
6 pub bwd: [f64; N],
7}
8
9/// Result of the model's optimization step.
10///
11/// Contains the optimal trading intensities (controls) for a given state.
12#[derive(Clone, Copy, Debug)]
13pub struct ControlOutput<const N: usize> {
14 /// Intensity to acquire inventory (+1 unit).
15 /// Typically the "Hit Bid" intensity for a market maker.
16 pub lambda_plus: [f64; N],
17 /// Intensity to shed inventory (-1 unit).
18 /// Typically the "Hit Ask" intensity for a market maker.
19 pub lambda_minus: [f64; N],
20 /// Net expected inventory flow rate (units per time).
21 /// $dq/dt = \lambda_+ - \lambda_-$.
22 pub flow: f64,
23}
24
25/// Interface for Optimal Control Models.
26///
27/// Defines the specific physics, market dynamics, and objective function of a financial model.
28/// Implementing this trait allows the model to be solved by any `Solver`.
29pub trait Model<const N: usize> {
30 /// The underlying stochastic process driving the continuous state dimensions.
31 /// Set to `()` for models without a corresponding market_model process.
32 type Process;
33
34 /// Returns the underlying stochastic process, if any.
35 fn process(&self) -> Self::Process;
36
37 /// Given the current state and value function gradients ($\nabla V$), computations the optimal controls.
38 ///
39 /// This is where the Hamiltonian optimization $\sup_{u} H(t, x, u, \nabla V)$ happens.
40 fn optimize(&self, state: &[f64; N], grads: &Gradients<N>) -> ControlOutput<N>;
41
42 /// Computes the terminal value function $V(T, x)$ (Final Condition).
43 /// Usually represents liquidation cost or final utility of wealth.
44 fn terminal(&self, state: &[f64; N]) -> f64;
45
46 /// Optional discount rate at the given state.
47 /// Default implementation returns 0.0.
48 fn discount_rate(&self, _state: &[f64; N]) -> f64 {
49 0.0
50 }
51
52 /// Optimization hint: Returns Some(r) if the discount rate is constant across all states.
53 /// Returns None if it depends on state.
54 /// Default implementation returns None (safe fallback).
55 fn constant_discount_rate(&self) -> Option<f64> {
56 None
57 }
58
59 /// Optional constraint application (e.g. for American options)
60 /// Default implementation does nothing.
61 fn apply_constraint(&self, _state: &[f64; N], value: f64) -> f64 {
62 value
63 }
64
65 /// Base order arrival rate $A$ used for intensity-to-spread conversion.
66 /// May depend on the current state (e.g. Hawkes lambda). Defaults to 1.0.
67 fn fill_rate_base(&self, _state: &[f64; N]) -> f64 {
68 1.0
69 }
70
71 /// Order fill decay parameter $\kappa$ used for intensity-to-spread conversion.
72 /// Defaults to 1.0 for non-market-making models. Override in market-making models.
73 fn fill_rate_decay(&self) -> f64 {
74 1.0
75 }
76
77 /// Simulates the next state for BSDE exploration.
78 /// Default implementation is a simple random walk: x' = x + sqrt(dt) * noise
79 fn next_step(&self, current_state: &[f64; N], dt: f64, noise: &[f64; N]) -> [f64; N] {
80 let mut next = *current_state;
81 let sqrt_dt = dt.sqrt();
82 for i in 0..N {
83 next[i] += sqrt_dt * noise[i];
84 }
85 next
86 }
87
88 /// Simulates the next state for Coupled FBSDE exploration where dynamics depend on control.
89 /// Default implementation falls back to `next_step` (Decoupled).
90 fn next_step_controlled(
91 &self,
92 current_state: &[f64; N],
93 _control: &ControlOutput<N>, // Control output from the model
94 dt: f64,
95 noise: &[f64; N],
96 ) -> [f64; N] {
97 self.next_step(current_state, dt, noise)
98 }
99
100 /// Indicates if a dimension is driven by Brownian diffusion.
101 /// If true, the BSDE backward step will skip the lambda_plus/minus drift term for this
102 /// dimension (it is already handled by the forward simulation noise).
103 fn is_diffusion_dimension(&self, _dim: usize) -> bool {
104 false
105 }
106
107 /// Indicates if a dimension takes only integer values (e.g. inventory q).
108 /// Controls BSDE initialization: integer dimensions are sampled discretely
109 /// while continuous dimensions (even if non-diffusion) are sampled with uniform noise.
110 /// Default: false (continuous).
111 fn is_integer_dimension(&self, _dim: usize) -> bool {
112 false
113 }
114
115 /// Optional transform for standard normal samples before forward stepping.
116 ///
117 /// This allows models to introduce cross-factor coupling (for example, correlated
118 /// Brownian factors) while keeping the solver sampling path allocation unchanged.
119 fn transform_noise(&self, _state: &[f64; N], noise: &[f64; N]) -> [f64; N] {
120 *noise
121 }
122
123 /// Physical finite-difference step for each state dimension used by BSDE gradient stencils.
124 ///
125 /// Discrete jump dimensions should usually keep step 1.0.
126 /// Continuous factors (for example variance or intensity) can override this with
127 /// their natural grid/scale spacing.
128 fn gradient_step(&self, _dim: usize) -> f64 {
129 1.0
130 }
131}