You have a strategy open in TradingView and the Strategy Tester shows one long number covering everything the chart has loaded. That might be three years of data or nine months, depending on your plan and timeframe, and either way you did not choose it. You want to know how the strategy behaved during 2023 only, or you want to keep the last six months untouched so you can check the idea on data it has never seen. The Strategy Tester has a date range selector, but it does not do what most people expect. This guide shows how to put the date range inside your Pine Script, where you control it, and how to avoid the handful of bugs that make a date filter quietly wrong.
What the Strategy Tester date range actually does
TradingView lets you pick a date range in the Strategy Tester overview, and it is worth being precise about what it changes. The script still runs from the first bar on the chart. Indicators warm up, variables accumulate state, and orders are evaluated the same way as before. The range selector then changes which trades are counted in the report. It filters the results rather than the execution.
That distinction matters when your strategy carries state. A strategy that uses a 200 bar moving average, a running count of consecutive losses, or a variable declared with var has already built that state by the time the selected range begins. In many cases that is exactly what you want, because the script behaves as it would have on that date in real life. In other cases it hides a problem: a trade that was opened before the range and closed inside it can show up in the report, and the starting equity for the range is whatever the account held at that moment, not the initial capital you configured.
A date filter written in Pine Script is different. You decide in code which bars are allowed to open trades. The strategy still calculates on every bar, so indicators are warm, but entries only happen inside the window, and you can choose what to do with a position that is still open when the window ends. Because the filter is part of the script, it travels with the code when you share it, it shows up as a setting in the inputs dialog, and anyone who loads the script gets the same test period you did.
The core pattern: time inputs and a window flag
Pine Script v6 has a dedicated input for this, input.time(). It returns a Unix timestamp in milliseconds, and TradingView renders it as a date and time picker in the settings dialog. You set the default with the timestamp() function, which turns a calendar date into the same millisecond value. Compare that value to the built-in time variable, which holds the opening time of the current bar, and you have a window test.
//@version=6
strategy("Date Range Window", overlay = true, initial_capital = 10000, default_qty_type = strategy.percent_of_equity, default_qty_value = 100)
startDate = input.time(timestamp("01 Jan 2023 00:00 +0000"), "Start date", group = "Test period")
endDate = input.time(timestamp("31 Dec 2023 23:59 +0000"), "End date", group = "Test period")
inWindow = time >= startDate and time <= endDate
fastMA = ta.sma(close, 20)
slowMA = ta.sma(close, 50)
longCond = ta.crossover(fastMA, slowMA)
if longCond and inWindow
strategy.entry("Long", strategy.long)
if ta.crossunder(fastMA, slowMA)
strategy.close("Long")
plot(fastMA, "Fast", color.teal)
plot(slowMA, "Slow", color.orange)
bgcolor(inWindow ? color.new(color.blue, 92) : na)Three details are worth reading closely. The first is the timezone in the timestamp() string. Writing +0000 at the end of the date string pins the default to UTC, so the same date means the same moment for everyone. If you leave the offset out, the string is interpreted in the chart's timezone, which means two people with different chart settings can get different windows from identical code.
The second is the order of operations on the exit. The entry is gated by inWindow, but the exit is not. That is deliberate. If you gate both, a position opened late in the window can never close once the window ends, and it sits there distorting the last trade. Entries get filtered, exits stay free, and later we will handle the end of the window explicitly.
The third is the bgcolor line. Shading the window on the chart costs one line and saves you from the most common mistake, which is trusting a date filter without ever looking at it. If the shading starts where you expected and ends where you expected, the comparison is working.
Why timestamp() beats year, month and dayofmonth
Many scripts you will find online build the window from the calendar variables instead. They compare year, month and dayofmonth against inputs, usually with a chain of and conditions. It works, but it is awkward to extend, because "after the 15th of March 2023" requires three coordinated comparisons and an easy off by one error when the month rolls over.
The timestamp comparison is a single check against a single number, so ranges compose cleanly. You can add a second window, take the union of two periods, or exclude one with a plain boolean expression. It also means the inputs dialog shows one date picker per boundary instead of six number boxes. The calendar variables still have a place when you want a rule that repeats, such as "only trade in the first week of each month", but for a test period input.time() is the tool.
If you need to build a timestamp from numbers rather than a string, timestamp() also accepts year, month, day, hour and minute arguments, and a version that takes a timezone as its first argument. Check the argument order in the TradingView reference before relying on it, because the timezone argument is optional and its position changes how the other values are read.
Closing the position when the window ends
A date filter that only blocks entries leaves a loose end. If a trade is open when the window ends, the strategy holds it until some exit rule fires, which may be well after the period you wanted to measure. The result is a final trade that straddles the boundary and drags in price movement from outside your window.
The cleaner approach is to detect the last bar of the window and flatten the position there. Because inWindow is a boolean series, you can compare it to its previous value: the window has just closed on the first bar where it is false but was true one bar earlier.
//@version=6
strategy("Date Range With Forced Exit", overlay = true, initial_capital = 10000, default_qty_type = strategy.percent_of_equity, default_qty_value = 100)
startDate = input.time(timestamp("01 Jan 2023 00:00 +0000"), "Start date", group = "Test period")
endDate = input.time(timestamp("31 Dec 2023 23:59 +0000"), "End date", group = "Test period")
closeAtEnd = input.bool(true, "Close open trade when the window ends", group = "Test period")
inWindow = time >= startDate and time <= endDate
windowJustClosed = inWindow[1] and not inWindow
rsi = ta.rsi(close, 14)
if inWindow and ta.crossover(rsi, 30)
strategy.entry("Long", strategy.long)
if ta.crossover(rsi, 60)
strategy.close("Long")
if closeAtEnd and windowJustClosed
strategy.close_all(comment = "Window end")
bgcolor(inWindow ? color.new(color.blue, 92) : na)One caution about strategy.close_all(). Orders in Pine Script fill on the next bar by default, so the closing order created on the first bar outside the window fills at the open of the bar after that. The flatten therefore happens a bar late, and the final trade includes that one extra bar. If you need the exit to land inside the window, set process_orders_on_close = true in the strategy() declaration, which fills orders at the close of the bar that generated them. That setting changes how every order fills, so read how it affects your other entries before turning it on. The related post on calc_on_every_tick covers a neighbouring setting that also changes when orders are evaluated.
Windows that cover recent data instead of fixed dates
A fixed calendar window is the right choice when you want to compare runs on identical data. Sometimes you want something relative instead, such as "only the last 90 days", which keeps moving as new bars arrive. For that, compare the bar time against the time of the last bar on the chart.
//@version=6
indicator("Last N Days Window", overlay = true)
daysBack = input.int(90, "Days to include", minval = 1)
msPerDay = 24 * 60 * 60 * 1000
cutoff = last_bar_time - daysBack * msPerDay
inWindow = time >= cutoff
bgcolor(inWindow ? color.new(color.green, 90) : na)
plotchar(inWindow, "In window", "", location.top)The built-in last_bar_time holds the opening time of the last bar of the dataset, so the cutoff is stable across the whole run. Two things to keep in mind. The window depends on how much history the chart has loaded, and on what the last bar is, so the same script can include different bars from one day to the next. And last_bar_time is a value the script knows ahead of time on historical bars, which is fine for shading or for filtering a test, but it is not something a live strategy should lean on to make decisions. For a fixed hold-out period, the date inputs are more honest.
Using date windows to hold back data
The most useful reason to add a date range is to split your data. If you tune every parameter against the whole history, the settings are fitted to that history, and the result tells you how well the parameters describe the past rather than how the idea might behave on unseen data. Keeping a later stretch of data aside and only looking at it once you have settled on settings is a basic discipline in strategy testing.
With inputs in the script, this takes seconds. Set the window to your tuning period, adjust the parameters, and write the settings down. Then change the dates to the period you held back and run it once. If the numbers look very different, the parameters were probably fitted to the first period. If they are roughly in line, you have more reason to take the idea seriously, though no result guarantees anything about the future. The walk-forward testing guide takes this further by rolling the split forward through time, and lookback bias explains a related way that history can flatter a script.
The Strategy Tester will report the same trade count and metrics for whatever the strategy actually traded, so check that the number of trades in the held-back window is large enough to say anything. A handful of trades is noise. A common rule of thumb is at least 50 to 100 trades before reading much into a percentage, and a short window on a daily chart will rarely reach that. If your window only produces a dozen trades, widen it or move to a lower timeframe before drawing conclusions. The Strategy Tester metrics reference explains what each number means once you have a window you trust.
Common mistakes that make the filter lie
Most date filter bugs are small, and they all produce a report that looks plausible. These are the ones worth checking before you trust a window.
The first is a timezone mismatch, which we covered above. If the default timestamps carry no offset, a collaborator in another timezone sees a window shifted by hours, and on a daily chart that can move a boundary by a whole bar.
The second is gating the exit as well as the entry. A trade opened inside the window that cannot close outside it stays open and makes the final trade look enormous or tiny depending on where price went. Filter entries, handle exits separately.
The third is forgetting indicator warm-up. If your window starts on the first bar of the chart, a 200 bar moving average has no value for 199 bars and the strategy cannot trade properly at the start. This is not caused by the date filter, but a window that begins too early will blame the filter for it. Start the window after the longest lookback in your script, or let the script run on earlier history and only trade inside the window.
The fourth is comparing time against a value the user can leave empty or set to a date before the chart has data. If the start date is earlier than the first loaded bar, the window silently begins at the first bar, which can make two different settings look identical. If the end date is in the future, the window stays open to the last bar, which is usually what you want, but it is worth knowing.
The fifth is a state variable that carries over. If you track something with var, such as a consecutive loss counter, it keeps counting across the whole chart. Gating only the entries leaves that counter running outside the window, so the state inside the window may not start from zero. If it should, reset it when the window opens using the same inWindow and inWindow[1] comparison.
The sixth is assuming a date window fixes lookahead problems. It does not. If a higher timeframe request or a pivot confirmation leaks future information into your signals, restricting the dates just restricts where the leak appears. The posts on pine-script lookahead and repainting cover that separately.
Showing the window on the chart and in the report
A test period you cannot see is a test period you will eventually misread. Beyond shading with bgcolor, it helps to put the window dates in a small table, so a screenshot of the chart carries its own context. The table dashboard guide walks through the full pattern, and the minimum version is a single cell:
//@version=6
indicator("Window Label", overlay = true)
startDate = input.time(timestamp("01 Jan 2023 00:00 +0000"), "Start date")
endDate = input.time(timestamp("31 Dec 2023 23:59 +0000"), "End date")
inWindow = time >= startDate and time <= endDate
var table info = table.new(position.top_right, 1, 1, bgcolor = color.new(color.gray, 80))
if barstate.islast
txt = "Window: " + str.format_time(startDate, "yyyy-MM-dd", "UTC") + " to " + str.format_time(endDate, "yyyy-MM-dd", "UTC")
table.cell(info, 0, 0, txt, text_color = color.white)
bgcolor(inWindow ? color.new(color.blue, 92) : na)The str.format_time() function turns a millisecond timestamp into readable text, and giving it the "UTC" timezone argument keeps the label consistent with the way the defaults were written. The table is created once with var and updated only on the last bar, so it adds almost no cost to the run.
Adding a date filter to a strategy you already have
If you already have a strategy and want to add a date window without disturbing it, the change is small but easy to get wrong in a long script. You need the two inputs near the top, the inWindow flag, and then the flag added to every entry condition. A script with several entry paths, long and short, scale-ins, and re-entries after a stop, can have five or six places where a trade can open. Miss one and the filter leaks.
This is a good use of an editor that makes targeted changes. In PineScripter, you can paste the strategy into the AI editor and ask in plain English: add start and end date inputs, only allow new entries inside the window, and close any open position on the last bar of the window. Because it edits only the lines that change, the rest of your logic stays as you wrote it, and the Code Changes view shows each edit line by line so you can check that every entry was covered. Every script it writes is checked for compile errors the same way TradingView checks it, and when a check finds an error the AI fixes it itself. You can then run the script on the chart inside PineScripter to see what it plots before pasting it into TradingView. PineScripter does not simulate trades or report returns, so for the strategy numbers you still use TradingView's Strategy Tester, which is where the date window you just added does its work. If you would rather write it by hand, everything above is enough to do it correctly. The indicator to strategy guide shows the wider conversion if you are starting from an indicator.
A checklist before you trust the window
Run through these before you read any number. Shade the window and confirm it starts and ends where you intended. Check that the default timestamps carry a timezone offset. Make sure entries are gated and exits are not, and decide whether the last trade should be force closed. Confirm the window begins after the longest indicator lookback. Count the trades inside the window and ask whether there are enough to mean anything. Reset any stateful variables when the window opens. If all of those pass, the date range is doing what you think it is, and you can move on to judging the strategy itself.
This article is educational content about writing Pine Script. PineScripter is a coding tool, not a financial advisor or a trading service, and nothing here is a recommendation to buy or sell anything. Test results describe the past only and say nothing certain about the future.
Frequently asked questions
Does the Strategy Tester date range change when my script runs?
As far as the report goes, it filters which trades are counted, while the script still runs from the first loaded bar. A date filter written in Pine Script controls when entries are allowed, so the logic itself changes. The two can be used together, but they answer slightly different questions.
What does input.time() return?
It returns a Unix timestamp in milliseconds and shows a date and time picker in the script settings. You compare it with the built-in time variable, which holds the opening time of the current bar.
Why does my date filter start a day early or late?
Usually a timezone mismatch. A timestamp() string without an offset is read in the chart's timezone, so pin the default with an offset such as +0000 and format any label with an explicit timezone.
Should the exit be inside the date window too?
Normally no. Gate entries and leave exits free, then close any open position explicitly when the window ends, so a trade cannot stay open forever outside the range.
How do I test only the last 90 days?
Compare time with last_bar_time minus 90 days in milliseconds. The window moves as new bars arrive and depends on how much history is loaded, so use fixed dates when you need repeatable comparisons.
Describe your indicator in plain English and get Pine Script that compiles →
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.