Skip to content

Use Stockcast in production

New to production with Stockcast?

Walkthrough step 9: From backtest to production walks through the whole cycle and compares the job with its backtest. This page is the short reference.

The same objects you simulate with can compute real orders. A production job has two steps each period: plan at the start, before sales, and close at the end, once the period's sales are known. A scheduler or an event can trigger each step. Stockcast provides the state, policy, and constraint objects. Your application keeps the data, calls the forecasting model, and sends the orders.

flowchart LR
    A["Last period's closed state<br/>(from your database)"] --> B["Plan<br/>advance_period → fit → predict → constraints"]
    B --> C["Send orders<br/>(ERP, supplier portal)"]
    B --> D["Save planned state"]
    D --> E["Close<br/>fulfill_demand with the period's sales"]
    E --> F["Save closed state"]
    F --> A

Plan: the period's order

Start from the state you saved at the last close, fit the target from a forecast made with data up to the previous period, and ask the policy:

import numpy as np
import pandas as pd

from stockcast.core import (
    ConstraintContext, InventoryStateDataFrame, MinimumOrderQuantity,
    OrderingConstraints, OrderMultiple,
)
from stockcast.policies import OrderUpToPolicy
from stockcast.utils import update_inventory_with_orders

LEAD_TIME, REVIEW_PERIOD = 2, 4
HORIZON = LEAD_TIME + REVIEW_PERIOD

# 1. Yesterday's closed state, as your database stores it.
yesterday = pd.Timestamp("2026-03-01")
saved = pd.DataFrame({
    "unique_id": ["tea_250g", "coffee_1kg"],
    "on_hand": [9.0, 14.0],
    "backorders": [0.0, 0.0],
    "in_transit": [np.array([12.0, 0.0]), np.array([0.0, 0.0])],
    "period": [55, 55],
    "date": [yesterday, yesterday],
})
state = InventoryStateDataFrame(saved, max_lead_time=LEAD_TIME, allow_backorders=False)

# 2. Open today: receive what is due. Today is a review day.
today_state = state.advance_period(freq="D", is_review_period=True)
now = int(today_state.get_dataframe()["period"].iloc[0])

# 3. Today's targets, from your forecasting service (data up to yesterday).
targets = pd.DataFrame({
    "unique_id": ["tea_250g", "coffee_1kg"],
    "target": [46.0, 25.0],
    "target_end_date": yesterday + pd.Timedelta(days=HORIZON),
})
policy = OrderUpToPolicy(lead_time=LEAD_TIME, review_period=REVIEW_PERIOD, freq="D",
                         service_level=0.95, allow_backorders=False,
                         date_column="target_end_date").fit(
    targets, target_column="target",
    forecast_origin=yesterday,
)

# 4. Propose, then apply the supplier's rules.
proposal = policy.predict(today_state, current_period=now)
rules = OrderingConstraints([MinimumOrderQuantity(6, mode="adjust"),
                             OrderMultiple(6, mode="adjust")])
accepted = rules.apply(
    proposal, ConstraintContext(inventory=today_state, policy=policy, decision_period=now),
)
accepted.audit[["unique_id", "requested_order_quantity", "constrained_order_quantity",
                "binding_constraints"]]
    unique_id  requested_order_quantity  constrained_order_quantity binding_constraints
0    tea_250g                      25.0                        30.0      order_multiple
1  coffee_1kg                      11.0                        12.0      order_multiple

At the start of the period 12 packs of tea arrived, lifting the shelf to 21. The policy asks for \(46 - 21 = 25\) and the case rule rounds it to 30. accepted.order is the OrderDecision to send, and accepted.audit explains every change the rules made. Record both, then place the order on the state:

planned_state = update_inventory_with_orders(today_state, accepted.order, policy=policy)

Close: the period's sales

When the period's sales arrive, serve them from the planned state and save the result for the next period:

sales = pd.DataFrame({
    "unique_id": ["tea_250g", "coffee_1kg"],
    "date": [yesterday + pd.Timedelta(days=1)] * 2,
    "y": [7.0, 4.0],
})
closed_state = planned_state.fulfill_demand(sales)
closed_state.get_dataframe()[["unique_id", "on_hand", "in_transit",
                              "latest_fulfilled", "latest_shortage"]]
    unique_id  on_hand   in_transit  latest_fulfilled  latest_shortage
0    tea_250g     14.0  [0.0, 30.0]               7.0              0.0
1  coffee_1kg     10.0  [0.0, 12.0]               4.0              0.0

closed_state.get_dataframe() is what you store. The next period's job starts from it.

  • Plan before sales. Fit forecasts on data up to the previous closed period, as the engine does in simulation. Keep the plan and the close as separate, saved steps, each with its own idempotency key, so a retry never orders or sells twice.
  • Backtest before changing parameters. A new target probability, lead time, or constraint can be replayed on recent history with SimulationEngine.run (or compared with run_comparison) before it goes live.
  • Keep the evidence. Store the targets with their metadata, the proposal, the constraint audit, and the accepted order. Together they explain each order the job places.
  • The engine for replays, primitives for live periods. The engine adds validation, callbacks, the event table, and the manifest, which suit backtests. A live job uses the primitives shown here.

Notebook 10 builds this job up for five M5 items: one day step by step, the plan and close functions, a store with idempotent phases and an order outbox, an event stream that triggers the phases for two weeks, a shadow backtest that monitors the job, and a morning rebuilt from a stock count and a late delivery.