Handling Timeframes in Pine Script v6: timeframe.period and timeframe.multiplier Explained

When building indicators or strategies that must behave differently depending on whether the user is viewing a 1-minute chart versus a daily chart, Pine Script v6 provides two essential built-in variables: timeframe.period and timeframe.multiplier. Understanding how these two variables work together allows you to write scripts that dynamically adapt their logic to any chart resolution without hardcoding timeframe assumptions.

1. What Are timeframe.period and timeframe.multiplier?

Pine Script v6 exposes several built-in timeframe.* variables that describe the chart's current resolution at runtime. The two most important for dynamic adaptation are:

Variable Type Description Example (1H chart)
timeframe.period simple string The full timeframe string of the current chart (e.g., "60", "D", "W") "60"
timeframe.multiplier simple int The numeric multiplier portion of the current timeframe 60

For a 1-hour chart, timeframe.period returns "60" (because Pine Script uses minutes as the unit for intraday timeframes — there is no "H" unit). timeframe.multiplier returns the integer 60. For a daily chart, timeframe.period returns "D" and timeframe.multiplier returns 1.

2. Timeframe Classification Booleans

Pine Script v6 also provides a set of boolean variables to classify the current chart's timeframe category. These are evaluated once at script load time (they are simple bool values):

Variable True When
timeframe.isseconds Chart is a seconds-based timeframe
timeframe.isminutes Chart is a minutes-based timeframe (includes hourly, e.g., 60, 240)
timeframe.isdaily Chart is a daily timeframe
timeframe.isweekly Chart is a weekly timeframe
timeframe.ismonthly Chart is a monthly timeframe

Important: There is no timeframe.ishours variable in Pine Script v6. Hourly charts are classified under timeframe.isminutes because they are represented as minute-based timeframes (e.g., 60 minutes = 1 hour).

3. Logic Flow: How the Script Adapts

The following diagram illustrates how a script uses timeframe.isminutes, timeframe.isdaily, and timeframe.multiplier to select an appropriate EMA length dynamically.

flowchart TD A[Script Loads] --> B[Read timeframe.multiplier Read timeframe.period] B --> C{Classify Timeframe} C -->|timeframe.isminutes == true| D[Intraday Path barsPerDay = int 390 / multiplier] C -->|timeframe.isdaily == true| E[Daily Path Use baseLength directly] C -->|timeframe.isweekly == true| F[Weekly Path Compress: baseLength / 4] C -->|timeframe.ismonthly == true| G[Monthly Path Compress: baseLength / 20] D --> H[dynamicLength = baseLength x barsPerDay] E --> I[dynamicLength = 20] F --> J[dynamicLength = max 2 baseLength / 4] G --> K[dynamicLength = max 2 baseLength / 20] H --> L[ta.ema close dynamicLength] I --> L J --> L K --> L L --> M[plot Dynamic EMA] M --> N[label.new on barstate.islast Show TF info and EMA length]

4. Practical Example: Dynamic EMA Length Based on Timeframe

The core use case is scaling indicator parameters proportionally to the chart's resolution. For example, a 20-period EMA on a daily chart represents 20 days of data. On a 5-minute chart, you might want to scale that to represent a similar amount of market time. Below is a complete, verified Pine Script v6 example.

📸 Human Intervention Point #1: Please add a screenshot here showing the indicator applied to a 5-minute chart and a daily chart side-by-side, demonstrating how the EMA length label changes dynamically.
🔽 [Click to expand] View Full Pine Script Code


//@version=6
indicator("Dynamic Timeframe Adapter", overlay = true)

// ─────────────────────────────────────────────────────────────
// SECTION 1: Read current chart timeframe properties
// ─────────────────────────────────────────────────────────────

// timeframe.period  → simple string: the full TF string (e.g. "5", "60", "D")
// timeframe.multiplier → simple int: the numeric part of the TF (e.g. 5, 60, 1)
string tfPeriod = timeframe.period
int    tfMult   = timeframe.multiplier

// ─────────────────────────────────────────────────────────────
// SECTION 2: Classify the timeframe using built-in booleans
// ─────────────────────────────────────────────────────────────

// timeframe.isminutes is true for ALL minute-based TFs including hourly (60, 240)
// There is NO timeframe.ishours in Pine Script v6
bool isMinutes = timeframe.isminutes
bool isDaily   = timeframe.isdaily
bool isWeekly  = timeframe.isweekly
bool isMonthly = timeframe.ismonthly

// ─────────────────────────────────────────────────────────────
// SECTION 3: Compute a dynamic EMA length
// ─────────────────────────────────────────────────────────────

// Base reference: 20 bars on a daily chart = 20 trading days
// For intraday charts, scale by how many bars fit in one trading day
// A standard trading day ≈ 390 minutes (US equities)
// For weekly/monthly, scale up proportionally

// NOTE: timeframe.multiplier is simple int, so arithmetic is safe.
// int(math.max(...)) is used to ensure the result is a valid int
// and satisfies the simple int requirement of ta.ema().

int baseLength = 20  // baseline EMA length (const int)

// Calculate bars per day based on timeframe
// For minutes: barsPerDay = 390 / tfMult (integer division via int())
// We use math.max to prevent division producing 0 or negative values
int barsPerDay = isMinutes ? int(math.max(1, 390 / tfMult)) : 1

// Scale the EMA length:
//   - Minutes: multiply base by barsPerDay to get ~20 days of bars
//   - Daily:   use base directly
//   - Weekly:  compress (base / 4, minimum 2)
//   - Monthly: compress further (base / 20, minimum 2)
int dynamicLength = (
        isMinutes ? int(math.max(2, baseLength * barsPerDay)) :
        isDaily ? baseLength :
        isWeekly ? int(math.max(2, baseLength / 4)) :
        isMonthly ? int(math.max(2, baseLength / 20)) :
        baseLength
    )

// ─────────────────────────────────────────────────────────────
// SECTION 4: Calculate and plot the dynamic EMA
// ─────────────────────────────────────────────────────────────

// ta.ema() requires simple int for its length parameter.
// dynamicLength is derived from simple int (timeframe.multiplier)
// and const int (baseLength), so it satisfies the simple int requirement.
float dynamicEma = ta.ema(close, dynamicLength)

plot(dynamicEma,
     title  = "Dynamic EMA",
     color  = color.new(color.orange, 0),
     linewidth = 2)

// ─────────────────────────────────────────────────────────────
// SECTION 5: Display a label on the last bar showing TF info
// ─────────────────────────────────────────────────────────────

// Build an informational string using str.tostring()
// str.tostring(value, "#.##") formats a float to 2 decimal places
// str.tostring(intValue) converts an int to its string representation
string tfCategory = (
                   isMinutes ? "Minutes" :
                   isDaily   ? "Daily"   :
                   isWeekly  ? "Weekly"  :
                   isMonthly ? "Monthly" :
                   "Other"
)

// Compute bars-per-day as float for display purposes
float barsPerDayDisplay = isMinutes ? 390.0 / tfMult : 1.0

string infoText = "TF: "        + tfPeriod +
                  " | Category: " + tfCategory +
                  " | Mult: "    + str.tostring(tfMult) +
                  " | Bars/Day: " + str.tostring(barsPerDayDisplay, "#.##") +
                  " | EMA Len: " + str.tostring(dynamicLength)

// Draw a label only on the most recent bar
if barstate.islast
    label.new(
         x        = bar_index,
         y        = dynamicEma,
         text     = infoText,
         style    = label.style_label_left,
         color    = color.new(color.orange, 80),
         textcolor = color.black,
         size     = size.large)

5. Key Formula: Scaling EMA Length for Intraday Charts

The mathematical relationship used to scale the EMA length for intraday timeframes is:

$$\text{dynamicLength} = \text{baseLength} \times \left\lfloor \frac{390}{\text{timeframe.multiplier}} \right\rfloor$$

Where:

  • $\text{baseLength} = 20$ (the reference number of daily bars)
  • $390$ is the approximate number of minutes in a standard US equity trading session
  • $\text{timeframe.multiplier}$ is the chart's bar size in minutes (e.g., 5 for a 5-minute chart, 60 for a 1-hour chart)
  • $\lfloor \cdot \rfloor$ denotes the floor (integer truncation) operation, implemented via int()

For a 5-minute chart: $\text{dynamicLength} = 20 \times \lfloor 390 / 5 \rfloor = 20 \times 78 = 1560$ bars

For a 60-minute chart: $\text{dynamicLength} = 20 \times \lfloor 390 / 60 \rfloor = 20 \times 6 = 120$ bars

For a daily chart: $\text{dynamicLength} = 20$ bars (used directly)

6. Using timeframe.period with request.security()

Another powerful application of timeframe.period is constructing higher-timeframe requests dynamically. Because timeframe.period is a simple string, it can be used directly in request.security() calls. The following snippet demonstrates a non-repainting higher-timeframe close fetch using the official pattern:

🔽 [Click to expand] View Higher-Timeframe Request Example
//@version=6
indicator("HTF Close via timeframe.period", overlay = true)

// ─────────────────────────────────────────────────────────────
// Fetch the confirmed (non-repainting) close from a user-selected
// higher timeframe. The official non-repainting pattern uses
// close[1] + barmerge.lookahead_on.
// ─────────────────────────────────────────────────────────────

// User selects a higher timeframe via input
string htfInput = input.timeframe("D", title = "Higher Timeframe")

// Non-repainting HTF close:
//   close[1] shifts to the previous confirmed bar
//   barmerge.lookahead_on maps the value to the start of the HTF period
//   Together they produce a stable, non-repainting series
float htfClose = request.security(
     syminfo.tickerid,
     htfInput,
     close[1],
     lookahead = barmerge.lookahead_on)

// Also display the current chart's timeframe period string as a label
// timeframe.period is a simple string — safe to use in str.* functions
string currentTF = timeframe.period

plot(htfClose,
     title     = "HTF Close (Non-Repainting)",
     color     = color.new(color.blue, 0),
     linewidth = 2)

// Show current TF info on the last bar
if barstate.islast
    label.new(
         x         = bar_index,
         y         = htfClose,
         text      = "Chart TF: " + currentTF + " | HTF: " + htfInput,
         style     = label.style_label_left,
         color     = color.new(color.blue, 80),
         textcolor = color.white,
         size      = size.small)

7. Common Pitfalls and Anti-Patterns

Pitfall Wrong Correct
Using "H" as an hour unit in timeframe strings "1H" "60" (60 minutes)
Checking for hourly with a non-existent variable timeframe.ishours timeframe.isminutes and timeframe.multiplier >= 60
Passing series int to ta.ema() length var int len = 20; if cond: len := x; ta.ema(close, len) Derive length from simple int sources only (e.g., timeframe.multiplier, input.int())
Formatting floats with str.tostring(math.round(v, 2)) str.tostring(math.round(value, 2)) str.tostring(value, "#.##")
Repainting HTF request request.security(sym, tf, close) request.security(sym, tf, close[1], lookahead = barmerge.lookahead_on)

8. Conclusion

  • timeframe.period returns the current chart's timeframe as a simple string (e.g., "5", "60", "D"), while timeframe.multiplier returns the numeric portion as a simple int — both are evaluated once at script load and are safe to use as ta.ema() length inputs after integer casting.
  • Hourly charts are classified under timeframe.isminutes (not a non-existent timeframe.ishours), and hour-based timeframe strings use minute notation (e.g., "60" for 1 hour, "240" for 4 hours).
  • The correct pattern for formatting float values to two decimal places in Pine Script v6 is str.tostring(value, "#.##") — not str.tostring(math.round(value, 2)).

Ideas for Script Advancement

  1. Multi-Timeframe Dashboard: Extend the dynamic EMA concept to plot EMAs from three different timeframes simultaneously on the same chart using request.security() with the non-repainting pattern, and use timeframe.period to auto-label each line.
  2. Adaptive ATR-Based Stop Loss: Replace the fixed ATR multiplier with a value derived from timeframe.multiplier, so that stop distances automatically widen on higher timeframes and tighten on lower ones, maintaining consistent risk-per-bar exposure across all chart resolutions.

Related Posts

Comments

Popular posts from this blog

How to Build a Volume Indicator in Pine Script v6: Buying & Selling Pressure Analysis

Pine Script v6 plot() Function: Complete Guide to Lines, Histograms, and Circles

SuperTrend Indicator in Pine Script v6: Canonical Formula, Flip Logic, and Non-Repainting Signals