Project 04
Tensors
Statistics
ML
Metrics
DataFrame

Time Series Stock Price Forecasting

A time series forecasting project for stock prices using synthetic market data, feature engineering, and regression baselines. It combines deepbox/ndarray, deepbox/stats, deepbox/ml, deepbox/metrics, deepbox/dataframe, deepbox/plot to deliver a larger production-style Deepbox workflow with reproducible outputs and documented architecture.

Features

  • Data Generation: Synthetic stock price data with realistic patterns
  • Feature Engineering: Technical indicators (MA, RSI, volatility)
  • Statistical Analysis: Return distribution and correlation analysis
  • Forecasting Models: Linear Regression and Ridge Regression baselines
  • Evaluation: RMSE, MAE, directional accuracy

Deepbox Modules Used

deepbox/ndarraydeepbox/statsdeepbox/mldeepbox/metricsdeepbox/dataframedeepbox/plot

Project Architecture

    Source Files

    index.ts
    1/**2 * Time Series Stock Price Forecasting3 *4 * Demonstrates time series analysis and forecasting using Deepbox.5 *6 * Deepbox Modules Used:7 * - deepbox/ndarray: Tensor operations8 * - deepbox/stats: Statistical analysis9 * - deepbox/ml: Regression models10 * - deepbox/metrics: Forecasting metrics11 * - deepbox/dataframe: Data manipulation12 * - deepbox/plot: Visualization13 */1415import { existsSync, mkdirSync, writeFileSync } from "node:fs";16import { isNumericTypedArray, isTypedArray } from "deepbox/core";17import { DataFrame } from "deepbox/dataframe";18import { mae, mse, r2Score, rmse } from "deepbox/metrics";19import { LinearRegression, Ridge } from "deepbox/ml";20import { tensor } from "deepbox/ndarray";21import { Figure } from "deepbox/plot";22import { StandardScaler } from "deepbox/preprocess";23import { mean, pearsonr, std } from "deepbox/stats";2425// ============================================================================26// Configuration27// ============================================================================2829const OUTPUT_DIR = "docs/projects/04-stock-price-forecasting/output";30const NUM_DAYS = 500;31const LOOKBACK = 20;3233const expectNumericTypedArray = (34  value: unknown35): Float32Array | Float64Array | Int32Array | Uint8Array => {36  if (!isTypedArray(value) || !isNumericTypedArray(value)) {37    throw new Error("Expected numeric typed array");38  }39  return value;40};4142// ============================================================================43// Data Generation44// ============================================================================4546/**47 * Generate synthetic stock price data with realistic patterns48 */49function generateStockData(50  numDays: number,51  seed = 4252): {53  dates: string[];54  prices: number[];55  returns: number[];56  volume: number[];57} {58  let randomSeed = seed;59  const seededRandom = () => {60    randomSeed = (randomSeed * 1103515245 + 12345) & 0x7fffffff;61    return randomSeed / 0x7fffffff;62  };6364  const randomNormal = (mean: number, std: number) => {65    const u1 = seededRandom();66    const u2 = seededRandom();67    const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);68    return mean + std * z;69  };7071  const dates: string[] = [];72  const prices: number[] = [];73  const returns: number[] = [];74  const volume: number[] = [];7576  let price = 100; // Starting price77  const startDate = new Date("2023-01-01");7879  for (let i = 0; i < numDays; i++) {80    // Generate date81    const date = new Date(startDate);82    date.setDate(date.getDate() + i);83    dates.push(date.toISOString().split("T")[0]);8485    // Generate return with trend, volatility, and mean reversion86    const trend = 0.0002; // Slight upward trend87    const volatility = 0.02;88    const meanReversion = (-0.01 * (price - 100)) / 100; // Pull back to 1008990    const dailyReturn = trend + meanReversion + randomNormal(0, volatility);91    returns.push(dailyReturn);9293    // Update price94    price = price * (1 + dailyReturn);95    prices.push(price);9697    // Generate volume with some correlation to volatility98    const baseVolume = 1000000;99    const volumeMultiplier = 1 + Math.abs(dailyReturn) * 10;100    volume.push(Math.round(baseVolume * volumeMultiplier * (0.8 + seededRandom() * 0.4)));101  }102103  return { dates, prices, returns, volume };104}105106/**107 * Calculate technical indicators108 */109function calculateIndicators(110  prices: number[],111  returns: number[]112): {113  sma20: number[];114  sma50: number[];115  volatility20: number[];116  rsi14: number[];117  momentum10: number[];118} {119  const n = prices.length;120121  // Simple Moving Averages122  const sma20: number[] = [];123  const sma50: number[] = [];124  for (let i = 0; i < n; i++) {125    if (i >= 19) {126      const window = prices.slice(i - 19, i + 1);127      sma20.push(window.reduce((a, b) => a + b, 0) / 20);128    } else {129      sma20.push(NaN);130    }131    if (i >= 49) {132      const window = prices.slice(i - 49, i + 1);133      sma50.push(window.reduce((a, b) => a + b, 0) / 50);134    } else {135      sma50.push(NaN);136    }137  }138139  // Rolling Volatility (20-day)140  const volatility20: number[] = [];141  for (let i = 0; i < n; i++) {142    if (i >= 19) {143      const window = returns.slice(i - 19, i + 1);144      const mean = window.reduce((a, b) => a + b, 0) / 20;145      const variance = window.reduce((sum, r) => sum + (r - mean) ** 2, 0) / 20;146      volatility20.push(Math.sqrt(variance) * Math.sqrt(252)); // Annualized147    } else {148      volatility20.push(NaN);149    }150  }151152  // RSI (14-day)153  const rsi14: number[] = [];154  for (let i = 0; i < n; i++) {155    if (i >= 14) {156      const gains: number[] = [];157      const losses: number[] = [];158      for (let j = i - 13; j <= i; j++) {159        if (returns[j] > 0) {160          gains.push(returns[j]);161          losses.push(0);162        } else {163          gains.push(0);164          losses.push(-returns[j]);165        }166      }167      const avgGain = gains.reduce((a, b) => a + b, 0) / 14;168      const avgLoss = losses.reduce((a, b) => a + b, 0) / 14;169      const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;170      rsi14.push(100 - 100 / (1 + rs));171    } else {172      rsi14.push(NaN);173    }174  }175176  // Momentum (10-day price change)177  const momentum10: number[] = [];178  for (let i = 0; i < n; i++) {179    if (i >= 10) {180      momentum10.push((prices[i] - prices[i - 10]) / prices[i - 10]);181    } else {182      momentum10.push(NaN);183    }184  }185186  return { sma20, sma50, volatility20, rsi14, momentum10 };187}188189/**190 * Create features for forecasting191 */192function createFeatures(193  prices: number[],194  returns: number[],195  indicators: ReturnType<typeof calculateIndicators>,196  lookback: number197): { X: number[][]; y: number[]; validIndices: number[] } {198  const X: number[][] = [];199  const y: number[] = [];200  const validIndices: number[] = [];201202  const startIdx = Math.max(lookback, 50); // Ensure all indicators are available203204  for (let i = startIdx; i < prices.length - 1; i++) {205    // Check if all indicators are valid206    if (207      Number.isNaN(indicators.sma20[i]) ||208      Number.isNaN(indicators.sma50[i]) ||209      Number.isNaN(indicators.volatility20[i]) ||210      Number.isNaN(indicators.rsi14[i]) ||211      Number.isNaN(indicators.momentum10[i])212    ) {213      continue;214    }215216    const features: number[] = [];217218    // Lagged returns219    for (let j = 0; j < lookback; j++) {220      features.push(returns[i - j]);221    }222223    // Technical indicators224    features.push((prices[i] - indicators.sma20[i]) / indicators.sma20[i]); // Price vs SMA20225    features.push((prices[i] - indicators.sma50[i]) / indicators.sma50[i]); // Price vs SMA50226    features.push(indicators.volatility20[i]);227    features.push(indicators.rsi14[i] / 100); // Normalize RSI228    features.push(indicators.momentum10[i]);229230    X.push(features);231    y.push(returns[i + 1]); // Next day return232    validIndices.push(i);233  }234235  return { X, y, validIndices };236}237238// ============================================================================239// Main Execution240// ============================================================================241242console.log("═".repeat(70));243console.log("  TIME SERIES STOCK PRICE FORECASTING");244console.log("  Built with Deepbox — TypeScript toolkit for AI & numerical computing");245console.log("═".repeat(70));246247// Create output directory248if (!existsSync(OUTPUT_DIR)) {249  mkdirSync(OUTPUT_DIR, { recursive: true });250}251252// ============================================================================253// Step 1: Generate Data254// ============================================================================255256console.log("\n📊 STEP 1: Generating Stock Price Data");257console.log("─".repeat(70));258259const { dates, prices, returns, volume: _volume } = generateStockData(NUM_DAYS);260261console.log(`\n✓ Generated ${NUM_DAYS} days of synthetic stock data`);262console.log(`  Date Range: ${dates[0]} to ${dates[dates.length - 1]}`);263console.log(`  Starting Price: $${prices[0].toFixed(2)}`);264console.log(`  Ending Price: $${prices[prices.length - 1].toFixed(2)}`);265console.log(`  Total Return: ${((prices[prices.length - 1] / prices[0] - 1) * 100).toFixed(2)}%`);266267// Basic statistics268const returnsTensor = tensor(returns);269const meanReturn = Number(mean(returnsTensor).data[0]);270const stdReturn = Number(std(returnsTensor).data[0]);271272console.log(`\nReturn Statistics:`);273console.log(`  Mean Daily Return: ${(meanReturn * 100).toFixed(4)}%`);274console.log(`  Daily Volatility:  ${(stdReturn * 100).toFixed(4)}%`);275console.log(`  Annualized Return: ${(meanReturn * 252 * 100).toFixed(2)}%`);276console.log(`  Annualized Vol:    ${(stdReturn * Math.sqrt(252) * 100).toFixed(2)}%`);277278// ============================================================================279// Step 2: Calculate Technical Indicators280// ============================================================================281282console.log("\n📈 STEP 2: Calculating Technical Indicators");283console.log("─".repeat(70));284285const indicators = calculateIndicators(prices, returns);286287// Show sample of indicators288const sampleIdx = prices.length - 1;289console.log(`\nLatest Indicators (${dates[sampleIdx]}):`);290console.log(`  Price:       $${prices[sampleIdx].toFixed(2)}`);291console.log(`  SMA(20):     $${indicators.sma20[sampleIdx].toFixed(2)}`);292console.log(`  SMA(50):     $${indicators.sma50[sampleIdx].toFixed(2)}`);293console.log(`  Volatility:  ${(indicators.volatility20[sampleIdx] * 100).toFixed(2)}%`);294console.log(`  RSI(14):     ${indicators.rsi14[sampleIdx].toFixed(2)}`);295console.log(`  Momentum:    ${(indicators.momentum10[sampleIdx] * 100).toFixed(2)}%`);296297// ============================================================================298// Step 3: Feature Engineering299// ============================================================================300301console.log("\n🔧 STEP 3: Feature Engineering");302console.log("─".repeat(70));303304const { X, y, validIndices: _validIndices } = createFeatures(prices, returns, indicators, LOOKBACK);305306console.log(`\n✓ Created feature matrix`);307console.log(`  Samples: ${X.length}`);308console.log(`  Features per sample: ${X[0].length}`);309console.log(`  Feature breakdown:`);310console.log(`    - ${LOOKBACK} lagged returns`);311console.log(`    - 5 technical indicators`);312313// ============================================================================314// Step 4: Train/Test Split315// ============================================================================316317console.log("\n📦 STEP 4: Train/Test Split");318console.log("─".repeat(70));319320const splitIdx = Math.floor(X.length * 0.8);321const XTrain = X.slice(0, splitIdx);322const XTest = X.slice(splitIdx);323const yTrain = y.slice(0, splitIdx);324const yTest = y.slice(splitIdx);325326console.log(`\n✓ Time-based split (no shuffle to preserve temporal order)`);327console.log(`  Training: ${XTrain.length} samples`);328console.log(`  Testing:  ${XTest.length} samples`);329330// Scale features331const scaler = new StandardScaler();332scaler.fit(tensor(XTrain));333const XTrainScaled = scaler.transform(tensor(XTrain));334const XTestScaled = scaler.transform(tensor(XTest));335336console.log(`✓ Applied StandardScaler`);337338// ============================================================================339// Step 5: Model Training340// ============================================================================341342console.log("\n🤖 STEP 5: Model Training");343console.log("─".repeat(70));344345// Linear Regression346console.log("\nTraining Linear Regression...");347const lr = new LinearRegression();348lr.fit(XTrainScaled, tensor(yTrain));349const yPredLR = lr.predict(XTestScaled);350351// Ridge Regression352console.log("Training Ridge Regression (alpha=0.1)...");353const ridge = new Ridge({ alpha: 0.1 });354ridge.fit(XTrainScaled, tensor(yTrain));355const yPredRidge = ridge.predict(XTestScaled);356357// Baseline: Predict mean358const meanPred = yTrain.reduce((a, b) => a + b, 0) / yTrain.length;359const yPredBaseline = tensor(Array(yTest.length).fill(meanPred));360361console.log("✓ Models trained");362363// ============================================================================364// Step 6: Model Evaluation365// ============================================================================366367console.log("\n📊 STEP 6: Model Evaluation");368console.log("─".repeat(70));369370const yTestTensor = tensor(yTest);371372// Calculate metrics373const results = [374  {375    name: "Mean Baseline",376    mse: mse(yTestTensor, yPredBaseline),377    rmse: rmse(yTestTensor, yPredBaseline),378    mae: mae(yTestTensor, yPredBaseline),379    r2: r2Score(yTestTensor, yPredBaseline),380    pred: yPredBaseline,381  },382  {383    name: "Linear Regression",384    mse: mse(yTestTensor, yPredLR),385    rmse: rmse(yTestTensor, yPredLR),386    mae: mae(yTestTensor, yPredLR),387    r2: r2Score(yTestTensor, yPredLR),388    pred: yPredLR,389  },390  {391    name: "Ridge Regression",392    mse: mse(yTestTensor, yPredRidge),393    rmse: rmse(yTestTensor, yPredRidge),394    mae: mae(yTestTensor, yPredRidge),395    r2: r2Score(yTestTensor, yPredRidge),396    pred: yPredRidge,397  },398];399400console.log("\nModel Comparison:\n");401const metricsDF = new DataFrame({402  Model: results.map((r) => r.name),403  "MSE (×10⁻⁵)": results.map((r) => (Number(r.mse) * 100000).toFixed(4)),404  "RMSE (%)": results.map((r) => (Number(r.rmse) * 100).toFixed(4)),405  "MAE (%)": results.map((r) => (Number(r.mae) * 100).toFixed(4)),406  "R² Score": results.map((r) => Number(r.r2).toFixed(4)),407});408console.log(metricsDF.toString());409410// Directional accuracy411console.log("\nDirectional Accuracy (predicting up/down):");412for (const result of results) {413  const predData = expectNumericTypedArray(result.pred.data);414  let correct = 0;415  for (let i = 0; i < yTest.length; i++) {416    const actualDirection = yTest[i] > 0 ? 1 : -1;417    const predDirection = predData[i] > 0 ? 1 : -1;418    if (actualDirection === predDirection) correct++;419  }420  const dirAcc = correct / yTest.length;421  console.log(`  ${result.name.padEnd(20)}: ${(dirAcc * 100).toFixed(2)}%`);422}423424// ============================================================================425// Step 7: Autocorrelation Analysis426// ============================================================================427428console.log("\n🔍 STEP 7: Autocorrelation Analysis");429console.log("─".repeat(70));430431// Calculate autocorrelation of returns432console.log("\nReturn Autocorrelation:");433for (const lag of [1, 5, 10, 20]) {434  const returns1 = returns.slice(0, returns.length - lag);435  const returns2 = returns.slice(lag);436  const [corr] = pearsonr(tensor(returns1), tensor(returns2));437  console.log(`  Lag ${String(lag).padStart(2)}: ${corr.toFixed(4)}`);438}439440// ============================================================================441// Step 8: Visualizations442// ============================================================================443444console.log("\n📊 STEP 8: Generating Visualizations");445console.log("─".repeat(70));446447// Price chart448try {449  const fig = new Figure({ width: 1000, height: 400 });450  const ax = fig.addAxes();451452  const xValues = Array.from({ length: prices.length }, (_, i) => i);453  ax.plot(tensor(xValues), tensor(prices), { color: "#2196F3", linewidth: 1 });454  ax.setTitle("Stock Price Over Time");455  ax.setXLabel("Day");456  ax.setYLabel("Price ($)");457458  const svg = fig.renderSVG();459  writeFileSync(`${OUTPUT_DIR}/price-chart.svg`, svg.svg);460  console.log(`  ✓ Saved: ${OUTPUT_DIR}/price-chart.svg`);461} catch (e) {462  console.log(`  ⚠ Could not generate price chart: ${e}`);463}464465// Returns distribution466try {467  const fig = new Figure({ width: 800, height: 400 });468  const ax = fig.addAxes();469470  // Create histogram471  const numBins = 30;472  const minRet = Math.min(...returns);473  const maxRet = Math.max(...returns);474  const binWidth = (maxRet - minRet) / numBins;475476  const bins: number[] = [];477  const counts: number[] = [];478  for (let i = 0; i < numBins; i++) {479    const binCenter = minRet + (i + 0.5) * binWidth;480    const count = returns.filter(481      (r) => r >= minRet + i * binWidth && r < minRet + (i + 1) * binWidth482    ).length;483    bins.push(binCenter * 100);484    counts.push(count);485  }486487  ax.bar(tensor(bins), tensor(counts), { color: "#4CAF50" });488  ax.setTitle("Daily Returns Distribution");489  ax.setXLabel("Return (%)");490  ax.setYLabel("Frequency");491492  const svg = fig.renderSVG();493  writeFileSync(`${OUTPUT_DIR}/returns-distribution.svg`, svg.svg);494  console.log(`  ✓ Saved: ${OUTPUT_DIR}/returns-distribution.svg`);495} catch (e) {496  console.log(`  ⚠ Could not generate returns distribution: ${e}`);497}498499// ============================================================================500// Step 9: Summary501// ============================================================================502503console.log(`\n${"═".repeat(70)}`);504console.log("  FORECASTING COMPLETE - SUMMARY");505console.log("═".repeat(70));506507const bestModel = results.reduce((best, r) => (Number(r.r2) > Number(best.r2) ? r : best));508509console.log("\n📌 Key Findings:\n");510console.log("  1. Data Overview:");511console.log(`     • ${NUM_DAYS} days of price data`);512console.log(`     • Annualized return: ${(meanReturn * 252 * 100).toFixed(2)}%`);513console.log(`     • Annualized volatility: ${(stdReturn * Math.sqrt(252) * 100).toFixed(2)}%`);514515console.log("\n  2. Best Model:");516console.log(`     • ${bestModel.name}`);517console.log(`     • R² Score: ${Number(bestModel.r2).toFixed(4)}`);518console.log(`     • RMSE: ${(Number(bestModel.rmse) * 100).toFixed(4)}%`);519520console.log("\n  3. Observations:");521console.log("     • Stock returns show low autocorrelation (efficient market)");522console.log("     • Technical indicators provide marginal improvement");523console.log("     • Directional prediction is challenging (~50% baseline)");524525console.log("\n📁 Output Files:");526console.log(`   • ${OUTPUT_DIR}/price-chart.svg`);527console.log(`   • ${OUTPUT_DIR}/returns-distribution.svg`);528529console.log(`\n${"═".repeat(70)}`);530console.log("  ✅ Stock Price Forecasting Complete!");531console.log("═".repeat(70));532

    Console Output

    $ npx tsx 04-stock-price-forecasting/index.ts
    See the example README for the expected console and artifact output.

    Key Takeaways

    • Data Generation: Synthetic stock price data with realistic patterns
    • Feature Engineering: Technical indicators (MA, RSI, volatility)
    • Statistical Analysis: Return distribution and correlation analysis
    • Forecasting Models: Linear Regression and Ridge Regression baselines
    • Use deepbox/ndarray for Tensor operations, time series processing.
    • Use deepbox/stats for Statistical tests, correlation analysis.