Quantumcona

ThinkorSwim ThinkScript: Custom Studies, Strategies & Automated Scans

We have a complicated relationship with ThinkorSwim. On one hand, it's hands-down the most feature-rich free trading platform available in the US. The charting is elite. The scanner alone is worth the price of admission — which is zero dollars. On the other hand, ThinkScript is a declarative language that fights you every step of the way if you're used to writing real code.

Quantumcona10 min read

ThinkorSwim ThinkScript: Custom Studies, Strategies & Automated Scans

We have a complicated relationship with ThinkorSwim. On one hand, it’s hands-down the most feature-rich free trading platform available in the US. The charting is elite. The options chain analysis is unmatched. The scanner alone is worth the price of admission — which is zero dollars. On the other hand, ThinkScript is… ThinkScript. A declarative language that fights you every step of the way if you’re used to writing real code in MQL4 or Python.

But here’s the thing. For US stocks and options, nothing else comes close without paying up. So after twelve years of building algos across every platform imaginable, we keep coming back to TOS for a specific set of tasks. Custom studies, strategy backtests, watchlist columns, scanner scripts, conditional order triggers — all inside one platform.

ThinkScript vs Other Languages

Feature ThinkScript MQL4 Pine Script NinjaScript
Cost Free Free Free/Pro Free/Paid
Language Style Declarative Procedural Declarative OOP (C#)
Live Execution Conditional Orders Full EA Webhooks Full Strategy
Backtesting Strategy tab Strategy Tester Built-in Strategy Analyzer
Debugging Sidebar values Print() pine_log() Output window
Best For US stocks/options Forex/CFDs All markets Futures

ThinkScript Basics

Two things you need to understand immediately. Everything else flows from this distinction.

Study vs Strategy

  • Study: A visual indicator on your chart. It draws lines, arrows, labels — but it doesn’t simulate trades. Think of it as “look but don’t touch.”
  • Strategy: This is where ThinkScript pretends to be a backtesting engine. Uses AddOrder calls to generate simulated entries and exits. The results are… directionally useful. Don’t bet your account on the exact numbers.

Your First Study

Classic EMA crossover. Pay attention to declare upper — miss that and your indicator shows up in its own pane like an oscillator, which is confusing when you’re plotting moving averages.

# EMA Crossover Study
# Plots fast and slow EMAs with crossover signals

declare upper;  # Plot on price chart (not separate pane)

input fastLength = 21;
input slowLength = 55;

def fastEMA = ExpAverage(close, fastLength);
def slowEMA = ExpAverage(close, slowLength);

plot Fast = fastEMA;
plot Slow = slowEMA;

Fast.SetDefaultColor(Color.CYAN);
Slow.SetDefaultColor(Color.ORANGE);

# Crossover arrows
plot BuySignal = if fastEMA crosses above slowEMA
                 then low - ATR(14) * 0.5 else Double.NaN;
plot SellSignal = if fastEMA crosses below slowEMA
                  then high + ATR(14) * 0.5 else Double.NaN;

BuySignal.SetPaintingStrategy(PaintingStrategy.ARROW_UP);
BuySignal.SetDefaultColor(Color.GREEN);
BuySignal.SetLineWeight(3);

SellSignal.SetPaintingStrategy(PaintingStrategy.ARROW_DOWN);
SellSignal.SetDefaultColor(Color.RED);
SellSignal.SetLineWeight(3);

Your First Strategy

Now the fun part — turning that study into something that simulates trades. The AddOrder function is ThinkScript’s version of trade execution, except nothing actually executes. It just paints entry/exit markers on the chart and generates a P&L report.

# EMA Crossover Strategy with ATR Stops
# For backtesting in TOS Strategy tab

input fastLength = 21;
input slowLength = 55;
input atrPeriod = 14;
input atrMultSL = 1.5;
input atrMultTP = 3.0;

def fastEMA = ExpAverage(close, fastLength);
def slowEMA = ExpAverage(close, slowLength);
def atrVal = ATR(atrPeriod);

# Entry conditions
def buySignal = fastEMA crosses above slowEMA;
def sellSignal = fastEMA crosses below slowEMA;

# Long trades
AddOrder(OrderType.BUY_TO_OPEN, buySignal, close,
         name = "Long Entry");
AddOrder(OrderType.SELL_TO_CLOSE, sellSignal, close,
         name = "Long Exit - Signal");
AddOrder(OrderType.SELL_TO_CLOSE,
         close <= EntryPrice() - atrVal * atrMultSL,
         close, name = "Long SL");
AddOrder(OrderType.SELL_TO_CLOSE,
         close >= EntryPrice() + atrVal * atrMultTP,
         close, name = "Long TP");

# Short trades
AddOrder(OrderType.SELL_TO_OPEN, sellSignal, close,
         name = "Short Entry");
AddOrder(OrderType.BUY_TO_CLOSE, buySignal, close,
         name = "Short Exit - Signal");
AddOrder(OrderType.BUY_TO_CLOSE,
         close >= EntryPrice() + atrVal * atrMultSL,
         close, name = "Short SL");
AddOrder(OrderType.BUY_TO_CLOSE,
         close <= EntryPrice() - atrVal * atrMultTP,
         close, name = "Short TP");

Advanced Patterns

Multi-Timeframe Confirmation

Multi-timeframe analysis in TOS is cleaner than what you get in most platforms — one line to pull a different aggregation period.

# Check daily trend while on intraday chart
input dailyEmaLength = 50;

def dailyClose = close(period = AggregationPeriod.DAY);
def dailyEMA = ExpAverage(dailyClose, dailyEmaLength);

def dailyUptrend = dailyClose > dailyEMA;
def dailyDowntrend = dailyClose < dailyEMA;

# Only take longs when daily trend is up
def filteredBuy = fastEMA crosses above slowEMA and dailyUptrend;

Session Time Filtering

If you’re not filtering by session time, you’re backtesting fantasy trades. Pre-market and after-hours have garbage liquidity on most names.

# Trade only during regular market hours
input startHour = 0930;
input endHour = 1545;

def currentTime = SecondsFromTime(startHour) >= 0
                  and SecondsTillTime(endHour) >= 0;

def buyCondition = buySignal and currentTime;

Range Breakout with Visual Box

One of our favorite setups to visualize in TOS. The Asian session range breakout. This script tracks the high and low during Asia hours, then plots breakout levels for the London/NY session.

# Asian session range breakout for forex
declare upper;

input rangeStartHour = 1800;   # 6 PM ET = Asia open
input rangeEndHour   = 0200;   # 2 AM ET = Asia close
input breakoutBuffer = 0.0005; # 5 pips for forex

# Detect range period
def inRange = if SecondsFromTime(rangeStartHour) >= 0
                 and SecondsTillTime(rangeEndHour) >= 0
              then 1
              else if SecondsTillTime(rangeStartHour) > 0
                      and SecondsFromTime(rangeEndHour) > 0
                   then 0
                   else 0;

# Track range high/low
def rangeHigh = if inRange and !inRange[1]
                then high
                else if inRange
                     then Max(rangeHigh[1], high)
                     else rangeHigh[1];

def rangeLow = if inRange and !inRange[1]
               then low
               else if inRange
                    then Min(rangeLow[1], low)
                    else rangeLow[1];

# Plot range levels after range closes
plot UpperLevel = if !inRange then rangeHigh + breakoutBuffer
                  else Double.NaN;
plot LowerLevel = if !inRange then rangeLow - breakoutBuffer
                  else Double.NaN;

UpperLevel.SetDefaultColor(Color.GREEN);
LowerLevel.SetDefaultColor(Color.RED);

# Breakout signals
def breakoutLong = !inRange and close > rangeHigh + breakoutBuffer
                   and close[1] <= rangeHigh + breakoutBuffer;
def breakoutShort = !inRange and close < rangeLow - breakoutBuffer
                    and close[1] >= rangeLow - breakoutBuffer;

Custom Watchlist Column

This right here is one of TOS’s killer features that people sleep on. Custom watchlist columns. You can run ThinkScript on every single ticker in your watchlist simultaneously. One column script scanning hundreds of symbols in real time? That’s genuinely powerful.

# Watchlist column: shows "LONG", "SHORT", or "FLAT"
# Add via: Setup → Customize... → Custom column

def fastEMA = ExpAverage(close, 21);
def slowEMA = ExpAverage(close, 55);

AddLabel(yes,
    if fastEMA > slowEMA then "LONG"
    else if fastEMA < slowEMA then "SHORT"
    else "FLAT",

    if fastEMA > slowEMA then Color.GREEN
    else if fastEMA < slowEMA then Color.RED
    else Color.GRAY
);

Scanner Scripts

The Stock Hacker scanner is, in our honest opinion, the single best reason to use ThinkorSwim. Period. You write a ThinkScript condition, and TOS scans the entire market in real time to find matches. We’ve built scanner scripts that surface trade setups we’d never find manually scrolling through charts.

# Scan: EMA crossover happened TODAY with volume surge

def fastEMA = ExpAverage(close, 21);
def slowEMA = ExpAverage(close, 55);
def crossToday = fastEMA crosses above slowEMA within 1 bars;
def volumeSurge = volume > Average(volume, 20) * 2;

plot scan = crossToday and volumeSurge;
# Scan: Stocks near 52-week high with strong momentum

def nearHigh = close >= Highest(high, 252) * 0.95;
def strongRSI = RSI(14) > 60 and RSI(14) < 80;
def aboveEMA = close > ExpAverage(close, 50);

plot scan = nearHigh and strongRSI and aboveEMA;

Conditional Orders (Semi-Automation)

Let us be blunt. ThinkorSwim does not do automated trading. Not really. If you’re coming from MT4 where your EA fires orders at 3 AM while you sleep — that doesn’t exist here. What TOS gives you instead is conditional orders, which is basically “we’ll place this order IF this study condition becomes true.” It’s a workaround. A clever one, but a workaround nonetheless.

Setting Up Conditional Orders

  1. Right-click chart → Buy Custom → With Triggers
  2. In the Order Entry:
    • Set: “Submit at” → market condition
    • Condition: “Study” → your custom study
    • Trigger: study signal == 1
  3. Set bracket order (SL + TP) automatically
  4. Enable: “Good ’til cancel” (GTC)

Alert-Based Workflow

The other approach — and honestly the one we use more often — is alerts. Build a study with your entry conditions, attach an alert, and let TOS notify you when something fires. Then you decide whether to pull the trigger manually.

# Custom study that triggers an alert

def buyCondition = fastEMA crosses above slowEMA
                   and close > ExpAverage(close, 200)
                   and volume > Average(volume, 20) * 1.5;

Alert(buyCondition, "BUY SIGNAL: EMA Crossover + Volume",
      Alert.BAR, Sound.Ding);

plot signal = if buyCondition then 1 else 0;

Backtesting in TOS

Real talk about TOS backtesting. It exists. It’s free. And it’s extremely limited compared to what you get in MetaTrader or even TradingView. No position sizing logic. No portfolio-level testing. No walk-forward optimization. The P&L report gives you surface-level stats without the depth serious algo traders need. That said — for a quick gut-check on whether your logic is completely broken? It does the job. Just don’t confuse a TOS backtest with rigorous strategy validation.

Adding a Strategy to Chart

  1. Studies → Edit Studies → Strategies tab
  2. Select your strategy → Add → OK
  3. Strategy appears with entry/exit markers on chart

Key Report Metrics

Right-click strategy signals on chart → Show Report

  • Total P&L
  • Number of trades
  • Win rate
  • Avg winner / Avg loser
  • Max drawdown
  • Profit factor

Important Limitations

Limitation Workaround
No position sizing in backtest Use fixed quantity; calculate size manually
No portfolio-level testing Test each symbol independently
No walk-forward optimization Export data → optimize in Python
Limited historical data Use OnDemand for extended replay
No tick-level precision Use 1-minute bars minimum

Useful ThinkScript Functions

# Price references
close, open, high, low, volume, hl2, hlc3, ohlc4

# Indicators (built-in)
SimpleMovingAvg(close, 20)
ExpAverage(close, 20)
RSI(14)
MACD(12, 26, 9)
ATR(14)
BollingerBands(20, 2)
Highest(high, 20)
Lowest(low, 20)

# Time functions
GetDay(), GetMonth(), GetYear()
GetDayOfWeek(GetYYYYMMDD())
SecondsFromTime(0930)
SecondsTillTime(1600)

# Cross detection
crosses above, crosses below
crosses above within N bars

# Aggregation (MTF)
close(period = AggregationPeriod.DAY)
close(period = AggregationPeriod.WEEK)

# Bar counting
BarNumber()
if BarNumber() == 1 then ... (first bar logic)

# State tracking with rec (recursive)
rec tradeCount = if buySignal then tradeCount[1] + 1
                 else tradeCount[1];

ThinkScript Tips

Twelve years of hard-won lessons, distilled. Don’t skip these.

  1. Use def for intermediate calculations — this isn’t optional advice, it’s survival. ThinkScript without def turns into spaghetti fast. Name your intermediate values. Your future self will thank you.
  2. rec for state tracking — ThinkScript has no variables in the traditional sense. Everything is a series. rec is the hack that lets you carry a value from one bar to the next. Ugly but essential.
  3. Double.NaN hides plots — want a plot to appear only when a condition is true? Return Double.NaN otherwise. This is ThinkScript’s version of “don’t draw anything here.” You’ll use it constantly.
  4. Test on OnDemand first — TOS has a market replay feature. Use it. Run your study against replayed data before you trust it with real money.
  5. Label everything — AddLabel() and AddChartBubble() are your Print() statements. ThinkScript has no console, no debugger, no breakpoints. Labels on the chart are literally all you’ve got.
  6. Watch computation limits — TOS will flat-out refuse to load your study if it’s too complex. If you hit it, you need to simplify — combine conditions, reduce lookback periods, remove redundant calculations.

ThinkScript is quirky, the documentation is a mess, and debugging is basically “stare at chart labels until enlightenment strikes.” If you’d rather skip the pain and get a working study, strategy, or scanner script built right the first time, that’s what we do at Quantumcona. Custom indicators, watchlist columns, conditional order setups — we’ve built hundreds of them.

Build My ThinkScript Study →

Article 19 of 20 in the Algo Trading Masterclass by Quantumcona. Next up: we wrap the whole series.