If you are moving from thinkorswim to TradingView, you are not looking for a syntax cheat sheet so much as a translation strategy. The two languages look similar enough to lull you into a direct find-and-replace, and then one thinkScript habit turns out to be impossible in Pine Script at all.
Short answer
There is no official automatic converter from thinkScript to Pine Script, so conversion is a manual translation. Most constructs map cleanly: thinkScript's input becomes Pine's input.* functions, def becomes a normal variable assignment, and plot() has a direct Pine equivalent. The one pattern that cannot be translated is thinkScript's negative offset, which references future bars. Pine Script has no equivalent, so any script relying on it must be refactored to look only backward from the current bar.
Key facts
- No official thinkScript-to-Pine-Script converter exists. TradingView ships a converter only between Pine Script versions, not between platforms.
- thinkScript declares intermediate values with def and chart outputs with plot; Pine Script uses ordinary variable assignment for intermediates and plot() for outputs.
- In thinkScript, a negative offset refers to future bars. Pine Script cannot reference future bars, so such logic must be rewritten to reference only past and current bars.
- thinkScript def variables can be recursive, referencing their own prior values. Pine Script expresses persistent state with var and the := reassignment operator instead.
- Pine Script namespaces its built-in functions: technical analysis functions live under ta, math helpers under math, and string helpers under str. thinkScript uses unqualified function names.
- Pine Script executes once per bar across the dataset and carries a series of values per variable, which is conceptually close to thinkScript but differs in how history is addressed.
- Pine Script requires plot() to be declared in global scope, so a thinkScript pattern that plots inside a conditional block must be restructured to pass a conditional value into a single global plot call.
| thinkScript | Pine Script v6 | Notes |
|---|---|---|
| input length = 10; | length = input.int(10, "Length") | Pine has typed input functions |
| def value = close - open; | value = close - open | Plain assignment, no keyword |
| plot Line = value; | plot(value, "Line") | Pine plot must be global |
| close[1] | close[1] | Positive offsets match |
| close[-1] | No equivalent | Future reference is not possible in Pine |
| Average(close, 20) | ta.sma(close, 20) | Namespace required |
| if cond then a else b; | cond ? a : b | Pine also has if expressions |
| AssignPriceColor(...) | barcolor(...) | Different function, similar intent |
Translate concepts, not lines
The first decision is the most important one: do not attempt a line-by-line transliteration. Write down what the indicator computes and displays in plain language, then implement that description in idiomatic Pine. This sounds slower and is reliably faster, because the two languages diverge in the places where a mechanical translation feels most confident.
Start by separating the script into three parts: the configurable inputs, the calculations, and the visual output. thinkScript encourages mixing these, particularly by plotting from inside conditional logic. Pine Script rewards keeping them apart, and it requires it for plots, which must be declared in global scope. Doing this separation on paper first means the Pine version has a clean structure rather than a translated one.
Then port the smallest working piece and compile it. Get the inputs and one calculation plotting correctly on a chart before you add filters, colors, alerts, or labels. Each subsequent addition then has an obvious cause if something breaks, which is far better than translating four hundred lines and facing a wall of errors with no idea which concept was mistranslated.
The mappings that are straightforward
Inputs translate directly but become typed. thinkScript's input length = 10 becomes input.int(10, "Length"), and Pine gives you input.float, input.bool, input.string, input.source, and others. Use the typed function that matches the value, and add minval or maxval where a bound is meaningful, because a bounded input prevents a class of runtime error later.
Intermediate values are simpler in Pine than in thinkScript. Where thinkScript needs def to declare a non-plotted variable, Pine just uses an assignment. There is no keyword to carry over, so def price = close becomes price = close. Positive history offsets are identical in both languages, so close[1] means the previous bar in each.
Built-in functions are where most compile errors appear, and the cause is almost always a missing namespace. Pine keeps technical analysis functions under ta, math helpers under math, and string helpers under str. A thinkScript Average(close, 20) becomes ta.sma(close, 20). If you see "could not find function or function reference" after a conversion, check the namespace before anything else.
//@version=6
indicator("Ported indicator", overlay = true)
// thinkScript: input length = 20;
length = input.int(20, "Length", minval = 1)
// thinkScript: def avg = Average(close, length);
avg = ta.sma(close, length)
// thinkScript: def wide = close - open;
wide = close - open
// thinkScript: plot Line = avg;
plot(avg, "Line", color.blue)This is the shape every ported script should reach first: typed inputs, named calculations, and a single global plot. Once this compiles on a chart, add one feature at a time.
The pattern that cannot be translated: negative offsets
This is the single most important thing to know before starting. In thinkScript, a negative offset refers to future bars. Pine Script has no equivalent, because a Pine script executing on a given bar cannot see bars that come after it. There is no flag, no workaround, and no function that grants this. Any thinkScript logic built on negative offsets has to be redesigned rather than translated.
The redesign is a shift in perspective: instead of asking "what happens two bars from now", express the same relationship from the point of view of a later bar looking back. A thinkScript condition that compares the current bar to a future bar becomes, in Pine, a condition evaluated two bars later that compares the current bar to two bars ago. The information is the same; the bar on which you can act is different.
That difference is not a technicality, it is the honest constraint. If a thinkScript study appeared to identify a turning point at the moment it happened by peeking one bar ahead, the Pine equivalent will confirm that turning point one bar later. Recognizing this early prevents a much worse outcome: building a Pine script that seems to reproduce the thinkScript behavior only because of a lookahead setting that leaks future data. Our guide to repainting covers why that class of bug is so misleading.
// thinkScript idea: compare this bar to a FUTURE bar (not portable)
// def wasHigher = close[-2] > close;
// Pine equivalent: evaluate the same relationship two bars later,
// looking backward from the current bar.
wasHigher = close > close[2]
// The comparison is identical; the bar you can act on is two bars later.Write a comment recording the shift whenever you do this. Six months later, the two-bar offset will look arbitrary unless the source explains that it came from a thinkScript future reference.
Persistent state: def recursion versus var
thinkScript allows a def variable to reference its own previous value, which makes running totals and stateful flags concise. Pine handles the same need differently, and the distinction is worth understanding rather than pattern-matching.
In Pine, a plain assignment re-executes on every bar, so it does not accumulate. To persist a value across bars you declare it with var, which initializes once on the first bar, and then update it with the := reassignment operator. That gives you a running counter, a last-signal price, or a state flag that survives from bar to bar.
Two practical cautions. First, declare a persisted variable in global scope if code outside a conditional block needs to read it; declaring it inside an if block makes it unavailable to a later plot. Second, initialize it to a value that honestly represents "nothing yet", usually na for a price, rather than a number like zero that will plot as a real level and mislead anyone reading the chart.
//@version=6
indicator("Persistent state", overlay = true)
// Declared once, survives across bars.
var float lastCrossPrice = na
var int crossCount = 0
fast = ta.sma(close, 10)
slow = ta.sma(close, 30)
crossed = ta.crossover(fast, slow)
if crossed
lastCrossPrice := close
crossCount += 1
plot(lastCrossPrice, "Last cross", color.orange, style = plot.style_stepline)The var declarations sit above the conditional, and the updates sit inside it. That ordering is what lets the global plot read the value while the update stays local to the event.
Visual output and the global-scope rule
thinkScript code frequently plots from inside conditional logic. Pine does not allow that: plot() must be declared in global scope. The correct translation keeps the condition and moves the decision into the value, so a single global plot receives either the value or na depending on the condition.
For markers on specific bars, plotshape() or plotchar() is the right tool, with the condition supplied as the argument that controls visibility. For colour changes, pass a conditional colour into the existing plot rather than declaring two plots and hiding one. thinkScript's AssignPriceColor has a rough Pine counterpart in barcolor(), which likewise belongs in global scope.
Labels and lines are the exception that causes confusion. They create drawing objects rather than declaring chart output, so they can be created conditionally. They come with their own limits: TradingView documents that the x point of a line, label, or box cannot be more than 9,999 bars in the past or more than 500 bars in the future relative to the bar that draws it. A thinkScript study that annotated liberally can therefore hit object limits when ported naively, so reuse and delete objects instead of creating one per bar.
A porting checklist worth following
Before writing Pine, audit the thinkScript source for the three things that force redesign: negative offsets, recursive def variables, and plots inside conditionals. Finding these first tells you how much of the job is translation and how much is redesign, which is the difference between an afternoon and a weekend.
While porting, work in verified layers. Inputs and one calculation, compile. Add the remaining calculations, compile. Add plots and colours, compile. Add alerts last, because an alert condition built on a misunderstood series is the hardest error to notice. Test on more than one symbol and timeframe, since Pine runtime errors depend on available history and a single clean chart proves little.
When the Pine version is working, compare it against the thinkorswim original on the same symbol and period. Expect small differences at the margins: indicator warm-up, session handling, and data sources are not identical between platforms. Investigate a difference in shape or timing, and accept a difference of a fraction on a moving average. Write down which differences you accepted and why.
Using AI for a cross-platform port
Porting is a task where AI genuinely helps, with one caveat that matters more here than in most work: a general model will happily produce Pine that mirrors your thinkScript structure, including patterns Pine does not support. The negative-offset case is the clearest example. Asked to convert a script with future references, a general assistant may quietly produce backward-looking code with different timing, or use a lookahead setting that leaks future data, and present either as an equivalent.
PineScripter is our product, and cross-version and cross-platform work is what its context model is built for. It retrieves the Pine Script v5 and v6 manuals while writing, so generated code references documented v6 signatures such as ta.sma and input.int rather than a half-remembered v5 or thinkScript shape. Its in-app lint findings can be routed through an AI correction loop, and its edits land as reviewable line-level changes, which suits a port built up in layers.
The judgment stays with you. Only you know whether a thinkScript study was relying on a future reference for display convenience or as the core of its signal, and that determines whether the Pine version is an acceptable equivalent or a different indicator. Use the tooling to write correct Pine quickly, then verify the ported output against the original in the Pine Editor and decide deliberately what you accept.
Frequently asked questions
Is there a converter from thinkScript to Pine Script?
No. There is no official automatic converter between thinkorswim's thinkScript and TradingView's Pine Script. TradingView's built-in converter only upgrades Pine Script between its own versions, such as v5 to v6. Cross-platform conversion is a manual translation.
What is the thinkScript equivalent of def in Pine Script?
A plain variable assignment. thinkScript uses def to declare a value that is calculated but not plotted, whereas Pine Script needs no keyword: def price = close becomes price = close. For a value that must persist across bars, Pine uses var to initialize once and := to reassign.
Why can I not convert thinkScript negative offsets to Pine Script?
In thinkScript a negative offset references future bars. Pine Script cannot reference future bars, because a script executing on a bar has no access to bars after it. Such logic must be refactored so the relationship is evaluated from a later bar looking backward, which means the signal confirms later than it appeared to in thinkScript.
Why does my converted thinkScript indicator fail with "could not find function"?
Pine Script namespaces its built-ins, so an unqualified function name from thinkScript will not resolve. Technical analysis functions live under ta, math helpers under math, and string helpers under str. For example, an average becomes ta.sma(close, length). Add the correct namespace before changing anything else.
Can I plot inside an if block when porting from thinkScript?
No. Pine Script requires plot() to be declared in global scope. Keep the condition where it belongs and move the decision into the plotted value, for example plot(condition ? value : na). For markers on specific bars use plotshape() with the condition as the visibility argument.
Will the ported Pine Script match thinkorswim exactly?
Usually not exactly. Indicator warm-up periods, session handling, and underlying data sources differ between platforms, so small numeric differences are normal. Differences in the shape or timing of a signal are worth investigating; differences of a fraction on a moving average generally are not. Compare both on the same symbol and period and record which differences you accepted.
The practical takeaway
Port the concept, not the syntax. Most of thinkScript maps cleanly once you add Pine namespaces and typed inputs, and the structural rules, global plots and var for persistence, make the result cleaner than the original. The one thing to settle before you start is whether the study relies on negative offsets, because Pine cannot see future bars and that logic must be redesigned to confirm a bar later rather than faked with a lookahead setting.
If writing idiomatic v6 from scratch is the slow part of the port, PineScripter is our product and retrieves the Pine manuals while generating, so the output uses current signatures rather than a thinkScript shape translated word for word.
Sources
- TradingView Pine Script v6 documentation
- TradingView Pine Script documentation: execution model
- thinkorswim Learning Center: thinkScript def reference
- TradingView Pine Script documentation: plots
Related reading: Pine Script vs ThinkScript vs EasyLanguage, repainting explained, var, varip, and persistent state, the global plot-scope rule.
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.