Skip to main content

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}