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:
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.
Recommended practice¶
- 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 withrun_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.