When building StockPulse, the core challenge was: how do you calculate reliable inventory forecasts when different products have wildly different sales patterns?
Here's a breakdown of the forecasting engine I built.
Three velocity algorithms, automatic selection
The SP_Velocity_Calculator class implements three approaches:
Simple Moving Average (SMA) — baseline, used when less than 90 days of data exists:
$velocity = array_sum($daily_sales) / count($daily_sales);
Weighted Moving Average (WMA) — recent days get higher weight:
$total_weight = ($n * ($n + 1)) / 2;
$weighted_sum = 0;
foreach ($daily_sales as $i => $sales) {
$weight = $i + 1;
$weighted_sum += $sales * $weight;
}
$velocity = $weighted_sum / $total_weight;
Exponential Smoothing — alpha=0.3 by default (configurable per product):
$smoothed = $daily_sales[0];
foreach (array_slice($daily_sales, 1) as $actual) {
$smoothed = ($alpha * $actual) + ((1 - $alpha) * $smoothed);
}
$velocity = $smoothed;
The engine auto-selects: less than 90 days of history → SMA, 90+ days → WMA.
Reorder point formula
public function calculate_reorder_point(
float $daily_velocity,
int $lead_time_days
): float {
$safety_stock = $daily_velocity * ($lead_time_days * 0.25);
return ($daily_velocity * $lead_time_days) + $safety_stock;
}
The 25% safety buffer accounts for supplier delays and demand spikes.
Seasonal adjustment
$seasonal_coefficient = $same_period_last_year / $last_month_average;
$adjusted_velocity = $base_velocity * $seasonal_coefficient;
This naturally handles Christmas spikes, summer slowdowns, etc.
HPOS compatibility
WooCommerce's High-Performance Order Storage moved orders to custom tables. Instead of querying wp_posts directly:
$orders = wc_get_orders([
'status' => ['wc-completed'],
'date_after' => date('Y-m-d', strtotime('-90 days')),
'return' => 'ids',
'limit' => -1,
]);
Caching architecture
All calculations are stored in wp_stockpulse_product_cache with a 24-hour TTL, refreshed daily via WP-Cron.
What's next
v1.1 will add purchase order PDF generator and ABC analysis.
Top comments (0)