When a client asks what next month’s sales will look like, the standard answers are an analytics plugin with a moving average, or pasting the last 60 days of revenue into a chat model. Both produce a number, but one is smoothed history and the other is a guess, and neither gives you a range you could order stock against.
There’s a third option for WooCommerce sales forecasting: a pretrained time series model you call like an API. I came to it through Shuai Guo’s walkthrough of Chronos-2 on Towards Data Science, which covers univariate, multivariate, covariate-informed and cold-start forecasting in one model, with working code throughout.
Why the usual answers fall short
A 28-day moving average lags every trend turn by construction. Ask it about a spike that started last week and it answers three weeks late. It also returns a single number, so when the owner asks how wrong the projection could be, you have nothing to say. Buying decisions live in that gap: if next month’s demand sits somewhere between 400 and 900 units, the order you place depends on which end you believe, and the average can’t tell you which.
Pasting numbers into a chat model is worse in a specific way. An LLM will emit plausible figures with no mechanism behind them and no calibrated interval. If you want to build on analytics that ask instead of count, the numbers need a stated error, and prose doesn’t provide one.
The old third path was a bespoke model per store: gather data, train, tune, validate, and end up with something that solves one problem on one dataset. Chronos-2 removes that loop. It’s a 120M-parameter encoder (T5-inspired, per the model card), Apache-2.0 licensed, about 478 MB on disk, and it runs on CPU. Context goes to 8192 steps and the horizon to 1024, well over a year of daily data. There is no training step: you pass history and get 21 quantile levels back.
Wiring WooCommerce sales forecasting to the model
The model consumes dataframes rather than tensors. The predict_df API wants long format: an id column, a timestamp column, the target, and any covariates as extra columns. The same weights handle univariate, multivariate, covariate-informed and cross-learning setups, and which one you get depends on how you assign the ids.
On the WordPress side the real work is assembling the series correctly. Order dates are stored in UTC, so aggregate in the store’s timezone or your daily totals drift across DST changes. A nightly WP-Cron event can build the context and stash it in a transient:
add_action( 'bbioon_run_forecast', function () {
$tz = new DateTimeZone( wp_timezone_string() );
$since = ( new DateTime( '-45 days', $tz ) )->setTime( 0, 0 );
$query = new WC_Order_Query( array(
'limit' => -1,
'status' => array( 'processing', 'completed' ),
'date_created' => '>' . $since->getTimestamp(),
) );
$totals = array();
foreach ( $query->get_orders() as $order ) {
$day = $order->get_date_created()->setTimezone( $tz )->format( 'Y-m-d' );
$totals[ $day ] = ( $totals[ $day ] ?? 0 ) + (float) $order->get_total();
}
// Fill gap days: the model expects a regular series.
$context = array();
$cursor = clone $since;
while ( $cursor < new DateTime( 'today', $tz ) ) {
$day = $cursor->format( 'Y-m-d' );
$context[] = array(
'item_id' => 'store',
'timestamp' => $day,
'revenue' => $totals[ $day ] ?? 0.0,
'is_weekend' => (int) in_array( (int) $cursor->format( 'N' ), array( 6, 7 ), true ),
);
$cursor->modify( '+1 day' );
}
set_transient( 'bbioon_forecast_context', $context, DAY_IN_SECONDS );
} );
Then add covariates the store knows in advance: is_weekend, public holidays for the shipping country, and the promo calendar if merchandising keeps one. In Guo’s synthetic building-demand case, weighted absolute percentage error dropped from 8.4% to 4.2% once known future covariates were passed, and a scheduled sale is the same kind of signal. The inference side stays small:
from chronos import Chronos2Pipeline
pipeline = Chronos2Pipeline.from_pretrained("amazon/chronos-2", device_map="cpu")
pred = pipeline.predict_df(
context_df, # from the WordPress job
future_df=future_covariates_df, # next 30 days: is_weekend, holiday, promo
prediction_length=30,
quantile_levels=[0.1, 0.5, 0.9],
id_column="item_id",
timestamp_column="timestamp",
target="revenue",
)
Keep the inference off the web server. Don’t try to load torch inside PHP; run the pipeline as a small HTTP service or a scheduled job and cache the 0.1/0.5/0.9 columns in an option, then let the dashboard read only from cache. This is the kind of pipeline I’d keep inside WP-Cron and a custom table rather than reaching for something like Airflow, at least until the store outgrows it. (I’ve only run this on single-site installs, so treat multisite as untested.)
For a product launched last week with three days of sales, pass its series alongside the sibling products’ histories and set cross_learning. The model borrows the weekday shape and peak size from its peers through in-context learning. In Guo’s cold-start test, WAPE went from 22.2% isolated to 16.7% with cross-learning, which is the difference between waiting a quarter for data and having something usable now.
If you want a forecast like this on a store you run or build for clients, from pulling the order series to the caching layer and the dashboard output, that’s the kind of work I take on.
Where zero-shot stops being enough
It isn’t free, and the article is good about the limits. Store data hits at least two of them. Promo-heavy series can look unlike anything in the pretraining mix, so a buy-one-get-one week may come out flattened. And anything past the context window is ignored, which matters once you go hourly, since 8192 steps is roughly a year of hourly data.
If under-forecasting a stockout costs ten times what over-ordering does, the symmetric quantile loss doesn’t reflect that asymmetry, and fixing it is a fine-tuning job. Before shipping anything, backtest: hold out the last four weeks, forecast them, and score against actuals. The fev package from the same repository does the scoring. The check I’d add is calibration: if actuals fall outside your 80% band much more than 20% of the time, the intervals are wrong and the median isn’t trustworthy either.