04
Time Series Stock Price Forecasting
Forecasts next-day returns on 500 days of synthetic prices with lagged returns and technical indicators as features. Linear regression and ridge regression are compared with a mean baseline.
Features
- Synthetic prices with a small upward trend, mean reversion to 100 and random noise
- Indicators: 20 and 50 day moving averages, 20 day volatility, 14 day RSI, 10 day momentum
- Time-ordered train and test split, with no shuffling
- Metrics: MSE, RMSE, MAE, R2 and directional accuracy
- Autocorrelation of returns with
pearsonr
What you will learn
- Synthetic prices with a small upward trend, mean reversion to 100 and random noise
- Indicators: 20 and 50 day moving averages, 20 day volatility, 14 day RSI, 10 day momentum
- Time-ordered train and test split, with no shuffling
- Metrics: MSE, RMSE, MAE, R2 and directional accuracy
- Use deepbox/ndarray for
tensor,toArray,item. - Use deepbox/stats for
mean,std,pearsonr.
Source
/** * Time Series Stock Price Forecasting * * Forecasts next-day returns on synthetic prices from lagged returns and technical indicators. * * Deepbox Modules Used: * - deepbox/ndarray: Tensor operations * - deepbox/stats: Statistical analysis * - deepbox/ml: Regression models * - deepbox/metrics: Forecasting metrics * - deepbox/dataframe: Data manipulation * - deepbox/plot: Visualization */ import { existsSync, mkdirSync, writeFileSync } from "node:fs";import { DataFrame } from "deepbox/dataframe";import { mae, mse, r2Score, rmse } from "deepbox/metrics";import { LinearRegression, Ridge } from "deepbox/ml";import { tensor } from "deepbox/ndarray";import { Figure } from "deepbox/plot";import { StandardScaler } from "deepbox/preprocess";import { mean, pearsonr, std } from "deepbox/stats"; // ============================================================================// Configuration// ============================================================================ const OUTPUT_DIR = "docs/projects/04-stock-price-forecasting/output";const NUM_DAYS = 500;const LOOKBACK = 20; // ============================================================================// Data Generation// ============================================================================ /** * Generate synthetic stock price data: a small trend, mean reversion to 100 and random noise */function generateStockData( numDays: number, seed = 42): { dates: string[]; prices: number[]; returns: number[]; volume: number[];} { let randomSeed = seed; const seededRandom = () => { randomSeed = (randomSeed * 1103515245 + 12345) & 0x7fffffff; return randomSeed / 0x7fffffff; }; const randomNormal = (mean: number, std: number) => { const u1 = seededRandom(); const u2 = seededRandom(); const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); return mean + std * z; }; const dates: string[] = []; const prices: number[] = []; const returns: number[] = []; const volume: number[] = []; let price = 100; // Starting price const startDate = new Date("2023-01-01"); for (let i = 0; i < numDays; i++) { // Generate date const date = new Date(startDate); date.setDate(date.getDate() + i); dates.push(date.toISOString().split("T")[0]); // Generate return with trend, volatility, and mean reversion const trend = 0.0002; // Slight upward trend const volatility = 0.02; const meanReversion = (-0.01 * (price - 100)) / 100; // Pull back to 100 const dailyReturn = trend + meanReversion + randomNormal(0, volatility); returns.push(dailyReturn); // Update price price = price * (1 + dailyReturn); prices.push(price); // Generate volume with some correlation to volatility const baseVolume = 1000000; const volumeMultiplier = 1 + Math.abs(dailyReturn) * 10; volume.push(Math.round(baseVolume * volumeMultiplier * (0.8 + seededRandom() * 0.4))); } return { dates, prices, returns, volume };} /** * Calculate technical indicators */function calculateIndicators( prices: number[], returns: number[]): { sma20: number[]; sma50: number[]; volatility20: number[]; rsi14: number[]; momentum10: number[];} { const n = prices.length; // Simple Moving Averages const sma20: number[] = []; const sma50: number[] = []; for (let i = 0; i < n; i++) { if (i >= 19) { const window = prices.slice(i - 19, i + 1); sma20.push(window.reduce((a, b) => a + b, 0) / 20); } else { sma20.push(NaN); } if (i >= 49) { const window = prices.slice(i - 49, i + 1); sma50.push(window.reduce((a, b) => a + b, 0) / 50); } else { sma50.push(NaN); } } // Rolling Volatility (20-day) const volatility20: number[] = []; for (let i = 0; i < n; i++) { if (i >= 19) { const window = returns.slice(i - 19, i + 1); const mean = window.reduce((a, b) => a + b, 0) / 20; const variance = window.reduce((sum, r) => sum + (r - mean) ** 2, 0) / 20; volatility20.push(Math.sqrt(variance) * Math.sqrt(252)); // Annualized } else { volatility20.push(NaN); } } // RSI (14-day) const rsi14: number[] = []; for (let i = 0; i < n; i++) { if (i >= 14) { const gains: number[] = []; const losses: number[] = []; for (let j = i - 13; j <= i; j++) { if (returns[j] > 0) { gains.push(returns[j]); losses.push(0); } else { gains.push(0); losses.push(-returns[j]); } } const avgGain = gains.reduce((a, b) => a + b, 0) / 14; const avgLoss = losses.reduce((a, b) => a + b, 0) / 14; const rs = avgLoss === 0 ? 100 : avgGain / avgLoss; rsi14.push(100 - 100 / (1 + rs)); } else { rsi14.push(NaN); } } // Momentum (10-day price change) const momentum10: number[] = []; for (let i = 0; i < n; i++) { if (i >= 10) { momentum10.push((prices[i] - prices[i - 10]) / prices[i - 10]); } else { momentum10.push(NaN); } } return { sma20, sma50, volatility20, rsi14, momentum10 };} /** * Create features for forecasting */function createFeatures( prices: number[], returns: number[], indicators: ReturnType<typeof calculateIndicators>, lookback: number): { X: number[][]; y: number[]; validIndices: number[] } { const X: number[][] = []; const y: number[] = []; const validIndices: number[] = []; const startIdx = Math.max(lookback, 50); // Start late enough for every indicator to be defined for (let i = startIdx; i < prices.length - 1; i++) { // Check if all indicators are valid if ( Number.isNaN(indicators.sma20[i]) || Number.isNaN(indicators.sma50[i]) || Number.isNaN(indicators.volatility20[i]) || Number.isNaN(indicators.rsi14[i]) || Number.isNaN(indicators.momentum10[i]) ) { continue; } const features: number[] = []; // Lagged returns for (let j = 0; j < lookback; j++) { features.push(returns[i - j]); } // Technical indicators features.push((prices[i] - indicators.sma20[i]) / indicators.sma20[i]); // Price vs SMA20 features.push((prices[i] - indicators.sma50[i]) / indicators.sma50[i]); // Price vs SMA50 features.push(indicators.volatility20[i]); features.push(indicators.rsi14[i] / 100); // Normalize RSI features.push(indicators.momentum10[i]); X.push(features); y.push(returns[i + 1]); // Next day return validIndices.push(i); } return { X, y, validIndices };} // ============================================================================// Main Execution// ============================================================================ console.log("═".repeat(70));console.log(" TIME SERIES STOCK PRICE FORECASTING");console.log(" Built with Deepbox: TypeScript toolkit for AI & numerical computing");console.log("═".repeat(70)); // Create output directoryif (!existsSync(OUTPUT_DIR)) { mkdirSync(OUTPUT_DIR, { recursive: true });} // ============================================================================// Step 1: Generate Data// ============================================================================ console.log("\nSTEP 1: Generating Stock Price Data");console.log("─".repeat(70)); const { dates, prices, returns, volume: _volume } = generateStockData(NUM_DAYS); console.log(`\nGenerated ${NUM_DAYS} days of synthetic stock data`);console.log(` Date Range: ${dates[0]} to ${dates[dates.length - 1]}`);console.log(` Starting Price: $${prices[0].toFixed(2)}`);console.log(` Ending Price: $${prices[prices.length - 1].toFixed(2)}`);console.log(` Total Return: ${((prices[prices.length - 1] / prices[0] - 1) * 100).toFixed(2)}%`); // Basic statisticsconst returnsTensor = tensor(returns);const meanReturn = Number(mean(returnsTensor).item());const stdReturn = Number(std(returnsTensor).item()); console.log(`\nReturn Statistics:`);console.log(` Mean Daily Return: ${(meanReturn * 100).toFixed(4)}%`);console.log(` Daily Volatility: ${(stdReturn * 100).toFixed(4)}%`);console.log(` Annualized Return: ${(meanReturn * 252 * 100).toFixed(2)}%`);console.log(` Annualized Vol: ${(stdReturn * Math.sqrt(252) * 100).toFixed(2)}%`); // ============================================================================// Step 2: Calculate Technical Indicators// ============================================================================ console.log("\nSTEP 2: Calculating Technical Indicators");console.log("─".repeat(70)); const indicators = calculateIndicators(prices, returns); // Show sample of indicatorsconst sampleIdx = prices.length - 1;console.log(`\nLatest Indicators (${dates[sampleIdx]}):`);console.log(` Price: $${prices[sampleIdx].toFixed(2)}`);console.log(` SMA(20): $${indicators.sma20[sampleIdx].toFixed(2)}`);console.log(` SMA(50): $${indicators.sma50[sampleIdx].toFixed(2)}`);console.log(` Volatility: ${(indicators.volatility20[sampleIdx] * 100).toFixed(2)}%`);console.log(` RSI(14): ${indicators.rsi14[sampleIdx].toFixed(2)}`);console.log(` Momentum: ${(indicators.momentum10[sampleIdx] * 100).toFixed(2)}%`); // ============================================================================// Step 3: Feature Engineering// ============================================================================ console.log("\nSTEP 3: Feature Engineering");console.log("─".repeat(70)); const { X, y, validIndices: _validIndices } = createFeatures(prices, returns, indicators, LOOKBACK); console.log(`\nCreated feature matrix`);console.log(` Samples: ${X.length}`);console.log(` Features per sample: ${X[0].length}`);console.log(` Feature breakdown:`);console.log(` - ${LOOKBACK} lagged returns`);console.log(` - 5 technical indicators`); // ============================================================================// Step 4: Train/Test Split// ============================================================================ console.log("\nSTEP 4: Train/Test Split");console.log("─".repeat(70)); const splitIdx = Math.floor(X.length * 0.8);const XTrain = X.slice(0, splitIdx);const XTest = X.slice(splitIdx);const yTrain = y.slice(0, splitIdx);const yTest = y.slice(splitIdx); console.log(`\nTime-based split (no shuffle to preserve temporal order)`);console.log(` Training: ${XTrain.length} samples`);console.log(` Testing: ${XTest.length} samples`); // Scale featuresconst scaler = new StandardScaler();scaler.fit(tensor(XTrain));const XTrainScaled = scaler.transform(tensor(XTrain));const XTestScaled = scaler.transform(tensor(XTest)); console.log(`Applied StandardScaler`); // ============================================================================// Step 5: Model Training// ============================================================================ console.log("\nSTEP 5: Model Training");console.log("─".repeat(70)); // Linear Regressionconsole.log("\nTraining Linear Regression...");const lr = new LinearRegression();lr.fit(XTrainScaled, tensor(yTrain));const yPredLR = lr.predict(XTestScaled); // Ridge Regressionconsole.log("Training Ridge Regression (alpha=0.1)...");const ridge = new Ridge({ alpha: 0.1 });ridge.fit(XTrainScaled, tensor(yTrain));const yPredRidge = ridge.predict(XTestScaled); // Baseline: Predict meanconst meanPred = yTrain.reduce((a, b) => a + b, 0) / yTrain.length;const yPredBaseline = tensor(Array(yTest.length).fill(meanPred)); console.log("Both models trained"); // ============================================================================// Step 6: Model Evaluation// ============================================================================ console.log("\nSTEP 6: Model Evaluation");console.log("─".repeat(70)); const yTestTensor = tensor(yTest); // Calculate metricsconst results = [ { name: "Mean Baseline", mse: mse(yTestTensor, yPredBaseline), rmse: rmse(yTestTensor, yPredBaseline), mae: mae(yTestTensor, yPredBaseline), r2: r2Score(yTestTensor, yPredBaseline), pred: yPredBaseline, }, { name: "Linear Regression", mse: mse(yTestTensor, yPredLR), rmse: rmse(yTestTensor, yPredLR), mae: mae(yTestTensor, yPredLR), r2: r2Score(yTestTensor, yPredLR), pred: yPredLR, }, { name: "Ridge Regression", mse: mse(yTestTensor, yPredRidge), rmse: rmse(yTestTensor, yPredRidge), mae: mae(yTestTensor, yPredRidge), r2: r2Score(yTestTensor, yPredRidge), pred: yPredRidge, },]; console.log("\nModel Comparison:\n");const metricsDF = new DataFrame({ Model: results.map((r) => r.name), "MSE (×10⁻⁵)": results.map((r) => (r.mse * 100000).toFixed(4)), "RMSE (%)": results.map((r) => (r.rmse * 100).toFixed(4)), "MAE (%)": results.map((r) => (r.mae * 100).toFixed(4)), "R² Score": results.map((r) => r.r2.toFixed(4)),});console.log(metricsDF.toString()); // Directional accuracyconsole.log("\nDirectional Accuracy (predicting up/down):");for (const result of results) { const predData = result.pred.toArray() as number[]; let correct = 0; for (let i = 0; i < yTest.length; i++) { const actualDirection = yTest[i] > 0 ? 1 : -1; const predDirection = predData[i] > 0 ? 1 : -1; if (actualDirection === predDirection) correct++; } const dirAcc = correct / yTest.length; console.log(` ${result.name.padEnd(20)}: ${(dirAcc * 100).toFixed(2)}%`);} // ============================================================================// Step 7: Autocorrelation Analysis// ============================================================================ console.log("\nSTEP 7: Autocorrelation Analysis");console.log("─".repeat(70)); // Calculate autocorrelation of returnsconsole.log("\nReturn Autocorrelation:");for (const lag of [1, 5, 10, 20]) { const returns1 = returns.slice(0, returns.length - lag); const returns2 = returns.slice(lag); const [corr] = pearsonr(tensor(returns1), tensor(returns2)); console.log(` Lag ${String(lag).padStart(2)}: ${corr.toFixed(4)}`);} // ============================================================================// Step 8: Visualizations// ============================================================================ console.log("\nSTEP 8: Generating Visualizations");console.log("─".repeat(70)); // Price charttry { const fig = new Figure({ width: 1000, height: 400 }); const ax = fig.addAxes(); const xValues = Array.from({ length: prices.length }, (_, i) => i); ax.plot(tensor(xValues), tensor(prices), { color: "#2196F3", linewidth: 1 }); ax.setTitle("Stock Price Over Time"); ax.setXLabel("Day"); ax.setYLabel("Price ($)"); const svg = fig.renderSVG(); writeFileSync(`${OUTPUT_DIR}/price-chart.svg`, svg.svg); console.log(` Saved: ${OUTPUT_DIR}/price-chart.svg`);} catch (e) { console.log(` Warning: could not generate price chart: ${e}`);} // Returns distributiontry { const fig = new Figure({ width: 800, height: 400 }); const ax = fig.addAxes(); // Create histogram const numBins = 30; const minRet = Math.min(...returns); const maxRet = Math.max(...returns); const binWidth = (maxRet - minRet) / numBins; const bins: number[] = []; const counts: number[] = []; for (let i = 0; i < numBins; i++) { const binCenter = minRet + (i + 0.5) * binWidth; const count = returns.filter( (r) => r >= minRet + i * binWidth && r < minRet + (i + 1) * binWidth ).length; bins.push(binCenter * 100); counts.push(count); } ax.bar(tensor(bins), tensor(counts), { color: "#4CAF50" }); ax.setTitle("Daily Returns Distribution"); ax.setXLabel("Return (%)"); ax.setYLabel("Frequency"); const svg = fig.renderSVG(); writeFileSync(`${OUTPUT_DIR}/returns-distribution.svg`, svg.svg); console.log(` Saved: ${OUTPUT_DIR}/returns-distribution.svg`);} catch (e) { console.log(` Warning: could not generate returns distribution: ${e}`);} // ============================================================================// Step 9: Summary// ============================================================================ console.log(`\n${"═".repeat(70)}`);console.log(" FORECASTING COMPLETE - SUMMARY");console.log("═".repeat(70)); const bestModel = results.reduce((best, r) => (r.r2 > best.r2 ? r : best)); console.log("\nKey Findings:\n");console.log(" 1. Data Overview:");console.log(` • ${NUM_DAYS} days of price data`);console.log(` • Annualized return: ${(meanReturn * 252 * 100).toFixed(2)}%`);console.log(` • Annualized volatility: ${(stdReturn * Math.sqrt(252) * 100).toFixed(2)}%`); console.log("\n 2. Best Model:");console.log(` • ${bestModel.name}`);console.log(` • R² Score: ${bestModel.r2.toFixed(4)}`);console.log(` • RMSE: ${(bestModel.rmse * 100).toFixed(4)}%`); console.log("\n 3. Observations:");console.log(" • The synthetic returns are close to white noise, so autocorrelation is low");console.log(" • R² is near zero, so the models barely beat the mean baseline");console.log(" • Directional accuracy stays near 50%"); console.log("\nOutput Files:");console.log(` • ${OUTPUT_DIR}/price-chart.svg`);console.log(` • ${OUTPUT_DIR}/returns-distribution.svg`); console.log(`\n${"═".repeat(70)}`);console.log(" Stock Price Forecasting Complete!");console.log("═".repeat(70));Output
Return statistics, indicator values and the model comparison table on the console
`output/price-chart.svg`
`output/returns-distribution.svg`