VaultCharts
Trading Strategystockscrypto

Time-Series Momentum (12-1) Strategy

Academic momentum strategy. Uses 12-month lookback excluding the most recent month (12-1 factor model).

What is Time-Series Momentum (12-1)?

Academic momentum strategy based on long-term price continuation. Uses 12-month lookback excluding the most recent month (12-1 factor model). Best for stocks and crypto with sufficient history. Enters when momentum exceeds threshold; exits when momentum flips. Default 252 bars lookback, 21 skip.

Strategy Parameters

ParameterTypeDefaultDescription
lookbacknumber252Lookback period (e.g. 12 months)
skipnumber21Skip recent bars (e.g. 1 month)

Use Cases

  • Long-term momentum (weekly/monthly)
  • Academic 12-1 factor
  • Stocks and crypto with history
  • Position trading

Strategy Script (JavaScript)

This strategy runs in VaultCharts using the built-in strategy engine. Below is the full script in a readable format. You can copy it or run it directly in VaultCharts.

strategy.jsVaultCharts built-in
module.exports = {
  meta: {
    name: "Time-Series Momentum",
    params: {
      lookback: { type: "number", default: 252 },
      skip: { type: "number", default: 21 }
    }
  },
  compute: (data, params, utils) => {
    // Data sanitization
    const cleanData = data.filter(d => 
      d && 
      Number.isFinite(d.close) && 
      Number.isFinite(d.time) &&
      d.close > 0
    );
    
    if (!cleanData || cleanData.length === 0) {
      console.warn('[Time-Series Momentum] No valid data');
      return { signals: [] };
    }
    
    const lookback = params?.lookback ?? 252;
    const skip = params?.skip ?? 21;
    const requiredLength = lookback + skip;
    
    if (cleanData.length < requiredLength) {
      console.warn('[Time-Series Momentum] Insufficient data:', cleanData.length, 'required:', requiredLength);
      return { signals: [] };
    }
    
    const signals = [];
    let checkedCount = 0;
    let positiveCount = 0;
    
    // Start from requiredLength to ensure we have valid indices
    for (let i = requiredLength; i < cleanData.length; i++) {
      const candle = cleanData[i];
      if (!candle || candle.time === undefined) continue;
      
      const pastIdx = i - lookback - skip;
      const recentIdx = i - skip;
      
      // Bounds check
      if (pastIdx < 0 || recentIdx < 0 || pastIdx >= cleanData.length || recentIdx >= cleanData.length) continue;
      
      const past = cleanData[pastIdx];
      const recent = cleanData[recentIdx];
      
      if (!past || past.close === undefined || !recent || recent.close === undefined) continue;
      if (!Number.isFinite(past.close) || !Number.isFinite(recent.close)) continue;
      
      checkedCount++;
      
      const momentumReturn = (recent.close - past.close) / past.close;
      const momentumPositive = momentumReturn > 0;
      const momentumNegative = momentumReturn < 0;
      const momentumThreshold = 0.05; // 5%
      const strongMomentumUp = momentumReturn > momentumThreshold;
      const strongMomentumDown = momentumReturn < -momentumThreshold;
      
      if (momentumPositive) positiveCount++;
      
      // Get last signal to determine position state
      const lastSignal = signals.length > 0 ? signals[signals.length - 1] : null;
      const wasLong = lastSignal && lastSignal.type === 'entry' && lastSignal.direction === 'long';
      const wasShort = lastSignal && lastSignal.type === 'entry' && lastSignal.direction === 'short';
      
      // Entry signals
      if (strongMomentumUp && !wasLong && !wasShort) {
        signals.push({ type: "entry", direction: "long", time: candle.time, price: candle.close, index: i });
      } else if (strongMomentumDown && !wasLong && !wasShort) {
        signals.push({ type: "entry", direction: "short", time: candle.time, price: candle.close, index: i });
      }
      // Exit signals
      else if (momentumNegative && wasLong) {
        signals.push({ type: "exit", direction: "long", time: candle.time, price: candle.close, index: i });
      } else if (momentumPositive && wasShort) {
        signals.push({ type: "exit", direction: "short", time: candle.time, price: candle.close, index: i });
      }
    }
    
    console.log('[Time-Series Momentum] Analysis:', {
      dataLength: cleanData.length,
      requiredLength,
      checkedCount,
      positiveCount,
      signalsGenerated: signals.length
    });
    
    return { signals };
  }
};

Run Time-Series Momentum (12-1) in VaultCharts

VaultCharts includes this strategy as a built-in option. Backtest it, adjust parameters, and use it on your own data—all stored locally on your device.

Related Strategies

Explore More