Skip to content

Deep IV Factor Model

The DIVFM module implements the Deep Implied Volatility Factor Model from Gauthier, Godin & Legros (2025). The IV surface on a given day is modelled as a linear combination of \(p\) fixed latent functions learned by a neural network:

\[\sigma_t(M, \tau; \theta) = \mathbf{f}(M, \tau, X; \theta)\,\boldsymbol{\beta}_t = \sum_{i=1}^{p} \beta_{t,i}\,f_i(M, \tau, X; \theta)\]

where \(M = \frac{1}{\sqrt{\tau}}\log\!\left(\frac{K}{F_{t,\tau}}\right)\) is the time-scaled moneyness, \(\mathbf{f}\) is a feedforward neural network with fixed weights \(\theta\) shared across all days, and \(\boldsymbol{\beta}_t\) are daily coefficients fitted in closed form via OLS.

Inference (no torch required)

quantflow.options.divfm.DIVFMPricer pydantic-model

Bases: OptionPricerBase

Option pricer based on the Deep Implied Volatility Factor Model (DIVFM).

The IV surface on a given day is modelled as a linear combination of p fixed latent functions learned by a neural network:

\[\begin{equation} \sigma_t\left(m, \tau\right) = f\left(m, \tau; theta\right) \dot \beta_t \end{equation}\]

where M = log(K/F) / sqrt(tau) is the time-scaled moneyness, f is implemented by DIVFMWeights, and beta_t are daily coefficients computed in closed form via OLS.

Call prices are derived from the IV surface via Black-Scholes.

Usage

  1. Train a DIVFMNetwork and call its to_weights() method to obtain a DIVFMWeights instance.
  2. Construct this pricer with those weights.
  3. Call calibrate with the day's observed implied volatilities to fit beta_t.
  4. Use maturity, price etc. as normal.

Fields:

ttm pydantic-field

ttm

Cache for MaturityPricer for different time to maturity

weights pydantic-field

weights

Extracted weights of the trained DIVFM network. No torch dependency required at inference time

betas pydantic-field

betas

Daily OLS factor loadings beta_t, shape (num_factors,)

extra pydantic-field

extra = None

Current day's observable features X, shape (extra_features,). Broadcast across all grid points in _compute_maturity. Set automatically by calibrate() when extra is provided

reset

reset()

Clear the ttm cache

Source code in quantflow/options/pricer.py
def reset(self) -> None:
    """Clear the [ttm][quantflow.options.pricer.OptionPricerBase.ttm] cache"""
    self.ttm.clear()

maturity

maturity(ttm)

Get a MaturityPricer from cache or compute a new one and return it

Source code in quantflow/options/pricer.py
def maturity(self, ttm: float) -> MaturityPricer:
    """Get a [MaturityPricer][quantflow.options.pricer.MaturityPricer]
    from cache or compute a new one and return it"""
    ttm_int = int(TTM_FACTOR * ttm)
    if ttm_int not in self.ttm:
        ttmr = ttm_int / TTM_FACTOR
        self.ttm[ttm_int] = self._compute_maturity(ttmr)
    return self.ttm[ttm_int]

price

price(option_type, ttm, strike, forward)

Price a single option

This method will use the cache to get the maturity pricer if possible

PARAMETER DESCRIPTION
option_type

Type of the option (call or put)

TYPE: OptionType

ttm

Time to maturity

TYPE: float

strike

Strike price of the option

TYPE: float

forward

Forward price of the underlying

TYPE: float

Source code in quantflow/options/pricer.py
def price(
    self,
    option_type: Annotated[OptionType, Doc("Type of the option (call or put)")],
    ttm: Annotated[float, Doc("Time to maturity")],
    strike: Annotated[float, Doc("Strike price of the option")],
    forward: Annotated[float, Doc("Forward price of the underlying")],
) -> ModelOptionPrice:
    """Price a single option

    This method will use the cache to get the maturity pricer if possible
    """
    return self.maturity(ttm).price(option_type, strike, forward)

call_prices

call_prices(ttms, log_strikes)

Price a batch of call options.

Options are grouped by their ttm so each unique maturity pricer is retrieved (and cached) once and the corresponding log-strikes are interpolated in a single vectorised np.interp call.

PARAMETER DESCRIPTION
ttms

Vector of time to maturities

TYPE: FloatArray

log_strikes

Vector of log-strikes log(K/F)

TYPE: FloatArray

Source code in quantflow/options/pricer.py
def call_prices(
    self,
    ttms: Annotated[FloatArray, Doc("Vector of time to maturities")],
    log_strikes: Annotated[FloatArray, Doc("Vector of log-strikes log(K/F)")],
) -> FloatArray:
    """Price a batch of call options.

    Options are grouped by their `ttm` so each unique maturity pricer is
    retrieved (and cached) once and the corresponding log-strikes are
    interpolated in a single vectorised `np.interp` call.
    """
    out = np.empty_like(log_strikes, dtype=float)
    for ttm in np.unique(ttms):
        mask = ttms == ttm
        mat = self.maturity(float(ttm))
        out[mask] = mat.pricing.call_price(log_strikes[mask])
    return out

plot3d

plot3d(max_moneyness=1.0, support=51, ttm=None, dragmode='turntable', scene_camera=None, **kwargs)

Plot the implied vols surface

It requires plotly to be installed

Source code in quantflow/options/pricer.py
def plot3d(
    self,
    max_moneyness: float = 1.0,
    support: int = 51,
    ttm: FloatArray | None = None,
    dragmode: str = "turntable",
    scene_camera: dict | None = None,
    **kwargs: Any,
) -> Any:
    """Plot the implied vols surface

    It requires plotly to be installed
    """
    if ttm is None:
        ttm = np.arange(0.05, 1.0, 0.05)
    moneyness = np.linspace(-max_moneyness, max_moneyness, support)
    implied = np.zeros((len(ttm), len(moneyness)))
    for i, t in enumerate(ttm):
        maturity = self.maturity(cast(float, t))
        implied[i, :] = maturity.prices(moneyness * np.sqrt(t))["implied_vol"]
    properties: dict = dict(
        xaxis_title="moneyness",
        yaxis_title="TTM",
        colorscale="viridis",
        dragmode=dragmode,
        scene=dict(
            xaxis=dict(title="moneyness"),
            yaxis=dict(title="TTM"),
            zaxis=dict(title="implied_vol"),
        ),
        scene_camera=scene_camera or dict(eye=dict(x=1.2, y=-1.8, z=0.3)),
        contours=dict(
            x=dict(show=True, color="white"), y=dict(show=True, color="white")
        ),
    )
    properties.update(kwargs)
    return plot.plot3d(
        x=moneyness,
        y=ttm,
        z=implied,
        **properties,
    )

calibrate

calibrate(moneyness_ttm, ttm, implied_vols, extra=None)

Fit daily OLS coefficients from observed implied volatilities.

Given a set of options observed on a single day, computes the closed-form OLS estimate:

beta_t = (F^T F)^{-1} F^T IV_t

where F is the (N, p) matrix of factor values from the network.

Parameters

moneyness_ttm: Shape (N,). Time-scaled moneyness M = log(K/F) / sqrt(tau). ttm: Shape (N,). Time-to-maturity tau in years. implied_vols: Shape (N,). Observed implied volatilities. extra: Shape (N, extra_features) or None. Additional features passed to the network (e.g. time-to-earnings-announcement).

Source code in quantflow/options/divfm/pricer.py
def calibrate(
    self,
    moneyness_ttm: FloatArray,
    ttm: FloatArray,
    implied_vols: FloatArray,
    extra: FloatArray | None = None,
) -> None:
    """Fit daily OLS coefficients from observed implied volatilities.

    Given a set of options observed on a single day, computes the
    closed-form OLS estimate:

        beta_t = (F^T F)^{-1} F^T IV_t

    where F is the (N, p) matrix of factor values from the network.

    Parameters
    ----------
    moneyness_ttm:
        Shape (N,). Time-scaled moneyness M = log(K/F) / sqrt(tau).
    ttm:
        Shape (N,). Time-to-maturity tau in years.
    implied_vols:
        Shape (N,). Observed implied volatilities.
    extra:
        Shape (N, extra_features) or None. Additional features passed to
        the network (e.g. time-to-earnings-announcement).
    """
    extra_arr = np.asarray(extra, dtype=np.float32) if extra is not None else None
    F = self.weights.forward(
        np.asarray(moneyness_ttm, dtype=np.float32),
        np.asarray(ttm, dtype=np.float32),
        extra_arr,
    )
    self.betas = np.linalg.lstsq(F, implied_vols, rcond=None)[0]
    # Store the mean X across options as the day-level representative value
    # used when pricing on a grid in _compute_maturity
    self.extra = (
        extra_arr.mean(axis=0, keepdims=True) if extra_arr is not None else None
    )
    self.reset()

quantflow.options.divfm.DIVFMWeights pydantic-model

Bases: BaseModel

Extracted weights of a trained DIVFMNetwork.

Implements the full network forward pass in pure numpy so that DIVFMPricer has no torch dependency at inference time.

Obtain an instance from a trained network via DIVFMNetwork.to_weights.

Fields:

subnet_ttm pydantic-field

subnet_ttm

Weights for the time-to-maturity sub-network (f_2)

subnet_moneyness pydantic-field

subnet_moneyness

Weights for the moneyness sub-network (f_3)

subnet_joint pydantic-field

subnet_joint = None

Weights for the joint (M, tau) sub-network (f_4 ... f_p). None when num_factors == 3

num_factors pydantic-field

num_factors

Total number of factors p (including the constant f_1)

extra_features pydantic-field

extra_features = 0

Number of additional observable features X beyond (M, tau)

forward

forward(moneyness_ttm, ttm, extra=None)

Compute factor values for a batch of options.

Parameters

moneyness_ttm: Shape (N,). Time-scaled moneyness M = log(K/F) / sqrt(tau). ttm: Shape (N,). Time-to-maturity tau in years. extra: Shape (N, extra_features) or None. Additional observable features.

Returns

FloatArray Shape (N, num_factors). Factor values [f_1, f_2, ..., f_p].

Source code in quantflow/options/divfm/weights.py
def forward(
    self,
    moneyness_ttm: FloatArray,
    ttm: FloatArray,
    extra: FloatArray | None = None,
) -> FloatArray:
    """Compute factor values for a batch of options.

    Parameters
    ----------
    moneyness_ttm:
        Shape (N,). Time-scaled moneyness M = log(K/F) / sqrt(tau).
    ttm:
        Shape (N,). Time-to-maturity tau in years.
    extra:
        Shape (N, extra_features) or None. Additional observable features.

    Returns
    -------
    FloatArray
        Shape (N, num_factors). Factor values [f_1, f_2, ..., f_p].
    """
    N = len(moneyness_ttm)

    # f_1 = 1
    f1 = np.ones((N, 1), dtype=np.float32)

    # f_2(tau [, X])
    ttm_in: FloatArray = ttm[:, None]
    if extra is not None:
        ttm_in = np.concatenate([ttm_in, extra], axis=1)
    f2 = self.subnet_ttm.forward(ttm_in)

    # f_3(M)
    f3 = self.subnet_moneyness.forward(moneyness_ttm[:, None])

    parts: list[FloatArray] = [f1, f2, f3]

    # f_4 ... f_p (M, tau [, X])
    if self.subnet_joint is not None:
        joint_in: FloatArray = np.stack([moneyness_ttm, ttm], axis=1)
        if extra is not None:
            joint_in = np.concatenate([joint_in, extra], axis=1)
        parts.append(self.subnet_joint.forward(joint_in))

    return np.concatenate(parts, axis=1)

quantflow.options.divfm.weights.SubnetWeights pydantic-model

Bases: BaseModel

Extracted weights for one sub-network (hidden layers + output layer).

Fields:

layers pydantic-field

layers

Ordered list of layer weights from input to output

forward

forward(x)

Run the numpy forward pass for this subnet.

Source code in quantflow/options/divfm/weights.py
def forward(self, x: FloatArray) -> FloatArray:
    """Run the numpy forward pass for this subnet."""
    return _apply_subnet(x, self.layers)

quantflow.options.divfm.weights.LayerWeights pydantic-model

Bases: BaseModel

Weights for a single linear layer with batch normalization.

Combines the linear transform, optional sigmoid activation, and batch normalization into one unit matching the structure of each block in DIVFMNetwork.

Fields:

weight pydantic-field

weight

Linear weight matrix, shape (out, in)

bias pydantic-field

bias

Linear bias vector, shape (out,)

bn_mean pydantic-field

bn_mean

Batch norm running mean, shape (out,)

bn_var pydantic-field

bn_var

Batch norm running variance, shape (out,)

bn_gamma pydantic-field

bn_gamma = None

Batch norm learnable scale (gamma), shape (out,). None for fixed (affine=False) output normalization

bn_beta pydantic-field

bn_beta = None

Batch norm learnable shift (beta), shape (out,). None for fixed (affine=False) output normalization

bn_eps pydantic-field

bn_eps = 1e-05

Batch norm epsilon for numerical stability

apply_activation pydantic-field

apply_activation = True

Whether to apply sigmoid activation before batch norm. True for hidden layers, False for the output layer

Training (requires quantflow[ml])

quantflow.options.divfm.network.DIVFMNetwork

DIVFMNetwork(num_factors=5, hidden_size=32, num_hidden_layers=3, extra_features=0)

Bases: Module

Neural network implementing the latent factor functions

\[ f_p\left(m, \tau, X; \theta\right) \]

Produces \(P\) factor functions with the following structural constraints (as in gauthier):

  • \(f_1 = 1\) constant, not learned
  • \(f_2(\tau, X)\) depends only on time-to-maturity and optional extra features X
  • \(f_3(m)\) depends only on time-scaled moneyness
  • \(f_4, ..., f_p (m, \tau, X)\) unrestricted

These structural constraints improve interpretability by associating each factor with a specific dimension of the implied volatility surface.

The network uses sigmoid activations throughout to ensure the implied volatility surface is twice continuously differentiable in the strike dimension, which is required for a well-defined risk-neutral density.

PARAMETER DESCRIPTION
num_factors

Total number of factors p (including the constant \(f_1\)). Must be greater or equal 3 to satisfy the structural constraints

TYPE: int DEFAULT: 5

hidden_size

Number of neurons per hidden layer

TYPE: int DEFAULT: 32

num_hidden_layers

Number of hidden layers L - 2 (default 3 gives L=5 total)

TYPE: int DEFAULT: 3

extra_features

Number of additional observable features X beyond (M, tau), e.g. time-to-earnings-announcement

TYPE: int DEFAULT: 0

Source code in quantflow/options/divfm/network.py
def __init__(
    self,
    num_factors: Annotated[
        int,
        Doc(
            "Total number of factors p (including the constant $f_1$). "
            "Must be greater or equal 3 to satisfy the structural constraints"
        ),
    ] = 5,
    hidden_size: Annotated[
        int,
        Doc("Number of neurons per hidden layer"),
    ] = 32,
    num_hidden_layers: Annotated[
        int,
        Doc("Number of hidden layers L - 2 (default 3 gives L=5 total)"),
    ] = 3,
    extra_features: Annotated[
        int,
        Doc(
            "Number of additional observable features X beyond (M, tau),"
            " e.g. time-to-earnings-announcement"
        ),
    ] = 0,
) -> None:
    super().__init__()
    if num_factors < 3:
        raise ValueError(
            "num_factors must be at least 3 (constant + ttm + moneyness)"
        )
    self.num_factors = num_factors
    self.extra_features = extra_features

    # f_2: input is tau (+ optional extra features X)
    self.subnet_ttm = _make_subnet(
        input_size=1 + extra_features,
        hidden_size=hidden_size,
        num_hidden_layers=num_hidden_layers,
        output_size=1,
    )

    # f_3: input is M only (moneyness, no extra features by design)
    self.subnet_moneyness = _make_subnet(
        input_size=1,
        hidden_size=hidden_size,
        num_hidden_layers=num_hidden_layers,
        output_size=1,
    )

    # f_4 ... f_p: input is (M, tau) + optional extra features X
    num_joint = num_factors - 3
    if num_joint > 0:
        self.subnet_joint: nn.Module = _make_subnet(
            input_size=2 + extra_features,
            hidden_size=hidden_size,
            num_hidden_layers=num_hidden_layers,
            output_size=num_joint,
        )
    else:
        self.subnet_joint = nn.Identity()

num_factors instance-attribute

num_factors = num_factors

extra_features instance-attribute

extra_features = extra_features

subnet_ttm instance-attribute

subnet_ttm = _make_subnet(input_size=1 + extra_features, hidden_size=hidden_size, num_hidden_layers=num_hidden_layers, output_size=1)

subnet_moneyness instance-attribute

subnet_moneyness = _make_subnet(input_size=1, hidden_size=hidden_size, num_hidden_layers=num_hidden_layers, output_size=1)

subnet_joint instance-attribute

subnet_joint = _make_subnet(input_size=2 + extra_features, hidden_size=hidden_size, num_hidden_layers=num_hidden_layers, output_size=num_joint)

to_weights

to_weights()

Extract network weights into a DIVFMWeights instance for torch-free inference.

Source code in quantflow/options/divfm/network.py
def to_weights(self) -> DIVFMWeights:
    """Extract network weights into a
    [DIVFMWeights][quantflow.options.divfm.weights.DIVFMWeights] instance
    for torch-free inference."""
    return DIVFMWeights(
        subnet_ttm=_extract_subnet(self.subnet_ttm),
        subnet_moneyness=_extract_subnet(self.subnet_moneyness),
        subnet_joint=(
            _extract_subnet(self.subnet_joint)  # type: ignore[arg-type]
            if self.num_factors > 3
            else None
        ),
        num_factors=self.num_factors,
        extra_features=self.extra_features,
    )

forward

forward(moneyness_ttm, ttm, extra=None)

Compute factor values for a batch of options.

Returns shape (N, num_factors) with factor values [f_1, f_2, ..., f_p].

PARAMETER DESCRIPTION
moneyness_ttm

Shape (N,). Time-scaled moneyness M = log(K/F) / sqrt(tau)

TYPE: Tensor

ttm

Shape (N,). Time-to-maturity tau in years

TYPE: Tensor

extra

Shape (N, extra_features) or None. Additional observable features X

TYPE: Tensor | None DEFAULT: None

Source code in quantflow/options/divfm/network.py
def forward(
    self,
    moneyness_ttm: Annotated[
        torch.Tensor,
        Doc("Shape (N,). Time-scaled moneyness M = log(K/F) / sqrt(tau)"),
    ],
    ttm: Annotated[
        torch.Tensor,
        Doc("Shape (N,). Time-to-maturity tau in years"),
    ],
    extra: Annotated[
        torch.Tensor | None,
        Doc("Shape (N, extra_features) or None. Additional observable features X"),
    ] = None,
) -> torch.Tensor:
    """Compute factor values for a batch of options.

    Returns shape (N, num_factors) with factor values [f_1, f_2, ..., f_p].
    """
    N = moneyness_ttm.shape[0]

    # f_1 = 1
    f1 = torch.ones(N, 1, device=moneyness_ttm.device, dtype=moneyness_ttm.dtype)

    # f_2(tau [, X])
    ttm_input = ttm.unsqueeze(1)
    if extra is not None:
        ttm_input = torch.cat([ttm_input, extra], dim=1)
    f2 = self.subnet_ttm(ttm_input)

    # f_3(M)
    f3 = self.subnet_moneyness(moneyness_ttm.unsqueeze(1))

    parts = [f1, f2, f3]

    # f_4 ... f_p (M, tau [, X])
    if self.num_factors > 3:
        joint_input = torch.cat(
            [moneyness_ttm.unsqueeze(1), ttm.unsqueeze(1)]
            + ([extra] if extra is not None else []),
            dim=1,
        )
        parts.append(self.subnet_joint(joint_input))  # type: ignore[arg-type]

    return torch.cat(parts, dim=1)

quantflow.options.divfm.trainer.DIVFMTrainer

DIVFMTrainer(network, lr=0.001, batch_days=64, weight_decay=0.0, ridge=1e-06)

Training loop for DIVFMNetwork.

Implements the mini-batch procedure from Gauthier, Godin & Legros (2025): at each gradient step a random subset of days is sampled from the training set, OLS factor loadings are computed in closed form for each day, and the network weights theta are updated to minimise the total IV residual.

The OLS step is fully differentiable via the normal equations, so gradients flow through beta_t back into the network parameters theta.

PARAMETER DESCRIPTION
network

The network to train

TYPE: DIVFMNetwork

lr

Adam learning rate

TYPE: float DEFAULT: 0.001

batch_days

Number of days sampled per gradient step (J=64 in the paper)

TYPE: int DEFAULT: 64

weight_decay

L2 regularisation for Adam

TYPE: float DEFAULT: 0.0

ridge

Ridge penalty added to F^T F before solving the normal equations, for numerical stability

TYPE: float DEFAULT: 1e-06

Source code in quantflow/options/divfm/trainer.py
def __init__(
    self,
    network: Annotated[DIVFMNetwork, Doc("The network to train")],
    lr: Annotated[float, Doc("Adam learning rate")] = 1e-3,
    batch_days: Annotated[
        int,
        Doc("Number of days sampled per gradient step (J=64 in the paper)"),
    ] = 64,
    weight_decay: Annotated[float, Doc("L2 regularisation for Adam")] = 0.0,
    ridge: Annotated[
        float,
        Doc(
            "Ridge penalty added to F^T F before solving the normal equations,"
            " for numerical stability"
        ),
    ] = 1e-6,
) -> None:
    self.network = network
    self.batch_days = batch_days
    self.ridge = ridge
    self.optimizer = torch.optim.Adam(
        network.parameters(), lr=lr, weight_decay=weight_decay
    )

network instance-attribute

network = network

batch_days instance-attribute

batch_days = batch_days

ridge instance-attribute

ridge = ridge

optimizer instance-attribute

optimizer = Adam(parameters(), lr=lr, weight_decay=weight_decay)

step

step(days)

Perform a single gradient update step.

Samples batch_days distinct days, computes the OLS loss for each, and updates the network weights.

Returns the total loss for this step.

PARAMETER DESCRIPTION
days

Pool of training days to sample from

TYPE: Sequence[DayData]

Source code in quantflow/options/divfm/trainer.py
def step(
    self,
    days: Annotated[
        Sequence[DayData],
        Doc("Pool of training days to sample from"),
    ],
) -> float:
    """Perform a single gradient update step.

    Samples ``batch_days`` distinct days, computes the OLS loss for each,
    and updates the network weights.

    Returns the total loss for this step.
    """
    self.network.train()
    batch = random.sample(list(days), min(self.batch_days, len(days)))

    self.optimizer.zero_grad()
    loss: torch.Tensor = sum(  # type: ignore[assignment]
        _day_loss(self.network, day, self.ridge) for day in batch
    )
    loss.backward()  # type: ignore[no-untyped-call]
    self.optimizer.step()
    return loss.detach().item()

evaluate

evaluate(days)

Compute the average per-day loss without updating weights.

PARAMETER DESCRIPTION
days

Days to evaluate on

TYPE: Sequence[DayData]

Source code in quantflow/options/divfm/trainer.py
def evaluate(
    self,
    days: Annotated[Sequence[DayData], Doc("Days to evaluate on")],
) -> float:
    """Compute the average per-day loss without updating weights."""
    if not days:
        return 0.0
    self.network.eval()
    total = 0.0
    with torch.no_grad():
        for day in days:
            total += float(_day_loss(self.network, day, self.ridge))
    return total / len(days)

fit

fit(days, num_steps=1000, val_days=None, log_every=100)

Train the network for num_steps gradient steps.

At each step, batch_days distinct days are sampled from days, following the mini-batch procedure described in the paper.

Returns the list of per-step training losses.

PARAMETER DESCRIPTION
days

Training days

TYPE: Sequence[DayData]

num_steps

Number of gradient update steps

TYPE: int DEFAULT: 1000

val_days

Optional validation days for loss monitoring

TYPE: Sequence[DayData] | None DEFAULT: None

log_every

Print a progress line every this many steps (0 to disable)

TYPE: int DEFAULT: 100

Source code in quantflow/options/divfm/trainer.py
def fit(
    self,
    days: Annotated[Sequence[DayData], Doc("Training days")],
    num_steps: Annotated[
        int,
        Doc("Number of gradient update steps"),
    ] = 1000,
    val_days: Annotated[
        Sequence[DayData] | None,
        Doc("Optional validation days for loss monitoring"),
    ] = None,
    log_every: Annotated[
        int,
        Doc("Print a progress line every this many steps (0 to disable)"),
    ] = 100,
) -> list[float]:
    """Train the network for ``num_steps`` gradient steps.

    At each step, ``batch_days`` distinct days are sampled from ``days``,
    following the mini-batch procedure described in the paper.

    Returns the list of per-step training losses.
    """
    losses: list[float] = []
    for step_idx in range(1, num_steps + 1):
        loss = self.step(days)
        losses.append(loss)

        if log_every and step_idx % log_every == 0:
            msg = f"step {step_idx}/{num_steps}  loss={loss:.6f}"
            if val_days is not None:
                val_loss = self.evaluate(val_days)
                msg += f"  val_loss={val_loss:.6f}"
            print(msg)

    return losses

to_weights

to_weights()

Extract the trained network into a DIVFMWeights instance ready for torch-free inference.

Source code in quantflow/options/divfm/trainer.py
def to_weights(self) -> DIVFMWeights:
    """Extract the trained network into a
    [DIVFMWeights][quantflow.options.divfm.weights.DIVFMWeights] instance
    ready for torch-free inference."""
    self.network.eval()
    return self.network.to_weights()

quantflow.options.divfm.trainer.DayData dataclass

DayData(moneyness_ttm, ttm, implied_vols, extra=None)

Option data for a single trading day.

Used as the unit of input for DIVFMTrainer. Each instance holds all options observed on one day.

moneyness_ttm instance-attribute

moneyness_ttm

Shape (N,). Time-scaled moneyness M = log(K/F) / sqrt(tau).

ttm instance-attribute

ttm

Shape (N,). Time-to-maturity tau in years.

implied_vols instance-attribute

implied_vols

Shape (N,). Observed implied volatilities.

extra class-attribute instance-attribute

extra = None

Shape (N, extra_features) or None. Additional observable features X.