DuckScript Guide

Learn to write powerful trading analysis scripts with our simple, intuitive language

๐Ÿš€

Quick Start

Write your first script in seconds! Here's a simple example:

# Calculate price range for each bar
range = high - low

# Show results
output table

๐Ÿ’ก Tip: This calculates the difference between high and low prices for each period. Try it yourself!

โœจWhy QuantQuack DSL (DuckScript)?

๐ŸŽฏ

Deterministic

Same code always produces the same results. No surprises!

๐Ÿ“

Expressive

Clear, readable syntax that feels natural to write

โšก

Powerful

Built-in functions for all your technical analysis needs

๐Ÿ“ŠAvailable Columns

Full Reference โ†’

Your script automatically has access to these data columns. No setup needed!

๐Ÿ’ฐOHLCV Prices

openOpening price
highHighest price
lowLowest price
closeClosing price
volumeTrading volume

๐Ÿ“…Metadata

timestampUnix timestamp
symbolSymbol identifier

๐Ÿ”งStructural Columns

These are automatically generated based on your dataset metadata:

sessionSession (e.g., "premarket", "regular")
dateDate as string (e.g., "2025-01-15")
interval_idInterval identifier
interval_startInterval start timestamp
interval_indexGlobal sequential index that continues summing across dates and sessions (e.g., 1010, 1011, 1012...). Does NOT reset when date or session changes. Depends on interval_duration.
interval_numberFollows the sequence of interval_index but resets at the start of each session (1, 2, 3, 4, 5... per session). Depends on interval_duration.

๐Ÿ“Variables and Assignments

Create variables to store your calculations and make your code more readable:

range = high - low
ma = sma(close, 20)
signal = close > ma

๐Ÿ“Œ Important Rules

  • Variable names must start with a letter or underscore
  • Can contain letters, numbers, and underscores
  • Variables are calculated in order - use them after defining

๐Ÿ’ก Example: Multiple Variables

# Calculate moving averages
ma20 = sma(close, 20)
ma50 = sma(close, 50)

# Detect golden cross
golden_cross = ma20 > ma50
above_ma20 = close > ma20

# Combine conditions
signal = golden_cross and above_ma20

output table

๐Ÿ”ขOperators

Full Reference โ†’

โž• Arithmetic

+Addition
-Subtraction
*Multiplication
/Division

โš–๏ธ Comparison

>Greater than
<Less than
>=Greater or equal
==Equal
!=Not equal

๐Ÿ”— Logical

andLogical AND
orLogical OR
notLogical NOT (bool/series<bool>)

๐Ÿ’ก Practical Example

# Check multiple conditions
is_bullish = close > open
is_high_volume = volume > 1000

# Combine with logical operators
strong_bullish = is_bullish and is_high_volume

# Use not to invert conditions (works with series<bool>)
weak_bearish = not(is_bullish) and is_high_volume

output table

๐Ÿท๏ธTypes

Type Keywords โ†’

The DSL (DuckScript) supports different types of data. Most operations work automatically with series!

๐Ÿ”ค Primitive Types

int42, -10

Integer numbers

float3.14, -0.5

Floating-point numbers

booltrue, false

Boolean values

string"hello"

Text strings

๐Ÿ“ˆSeries Types

Series represent arrays of values over time. All built-in columns return series:

๐Ÿ”ข
series<int>

Series of integers

Example:

timestamp
๐Ÿ“Š
series<float>

Series of floats

Example:

close, high
โœ…
series<bool>

Series of booleans

Example:

close > open

๐Ÿ’ก Tip: Most operations work with series automatically. For example, close > open compares each bar's close to its open, producing a series of boolean values.

๐Ÿ”ขConstants and Literals

Use literal values directly in your code - no need to declare them first!

๐Ÿ”ข Numbers

42 (integer)
3.14 (float)

๐Ÿ“ Strings

"hello"
"premarket"

โœ… Booleans

true
false
# Using literals in your code
ma20 = sma(close, 20)           # 20 is an integer literal
threshold = 0.5                  # 0.5 is a float literal
is_enabled = true                # true is a boolean literal
session_name = "regular"         # "regular" is a string literal

# Using literals in expressions
breakout = high > high[1] * 1.005  # 1.005 is a float literal
output table

๐Ÿ”‘Keywords (reference)

Full list โ†’

Reserved words and syntax details: input, func, signal, output, stats, by, if/then/else, types. Donโ€™t use them as variable names (except signal).

inputfuncsignaloutputstatsbycountpercentavgasifthenelseintfloatboolstringtruefalse

๐Ÿ”ฃSymbols

View Operators โ†’

Special symbols used in the DSL (DuckScript) syntax:

โœ๏ธ Assignment & Access

=Assignment operator
[]Index access (historical values)
()Function calls and grouping
->Function return type

๐Ÿ“„ Punctuation

,Comma (separates arguments)
:Colon (used in stats by)
#Comment marker

โฎ๏ธHistorical Values

Access values from previous periods using brackets []:

prev_close = close[1]    # Previous period's close
prev_high = high[1]      # Previous period's high
two_periods_ago = close[2]  # Two periods ago

๐Ÿ“Œ Important Notes

  • [0] is the current value (you can omit it)
  • [1] is the previous period
  • [2] is two periods ago, and so on

๐Ÿ’ก Example: Breakout Detection

# Compare current high to previous high
prev_high = high[1]
breakout = high > prev_high

output table

โš™๏ธBuilt-in Functions

View Full API Reference (DuckScript) โ†’

Powerful functions ready to use! Click any function name to see detailed documentation.

๐ŸŽฏ Core Functions

sma(...)

Simple Moving Average

ma20 = sma(close, 20)

ema(...)

Exponential Moving Average

ema50 = ema(close, 50)

rsi(...)

Relative Strength Index (simplified approximation)

rsi14 = rsi(close, 14)

atr(...)

Average True Range (simplified)

volatility = atr(14)

vwap(...)

Volume Weighted Average Price

vwap_value = vwap()

๐Ÿ”ข Math Functions

abs(...)

Absolute value

abs_change = abs(close - open)

highest(...)

Maximum value in rolling window

highest_20 = highest(high, 20)

lowest(...)

Minimum value in rolling window

lowest_20 = lowest(low, 20)

๐Ÿ“… Date / Time Functions

Extract parts of date or timestamp: weekday, hour, minute, month, day_of_month, year. Use with date or timestamp. See API Reference (DuckScript) for full signatures.

weekday(date)

Day of week (0=Sunday, 1=Monday, ..., 6=Saturday)

day_of_week = weekday(date)

# Use in aggregations
stats by session, weekday(date):
  count(breakout)
  percent(breakout)
hour(timestamp) โ†’ 0โ€“23
minute(timestamp) โ†’ 0โ€“59
month(date) โ†’ 1โ€“12
day_of_month(date) โ†’ 1โ€“31
year(date) โ†’ year

๐Ÿ” Pattern Functions

count_consecutive(...)

Count consecutive bars where condition is true

consecutive_up = count_consecutive(up)

bars_since(...)

Count bars since last time condition was true

bars_since_breakout = bars_since(breakout)

crossover(...)

Detect when series1 crosses above series2

golden_cross = crossover(ma_fast, ma_slow)

crossunder(...)

Detect when series1 crosses below series2

death_cross = crossunder(ma_fast, ma_slow)

high_since(...)

Maximum high since last time condition was true

highest_since_entry = high_since(signal)

low_since(...)

Minimum low since last time condition was true

lowest_since_entry = low_since(signal)

max_since(...)

Maximum value of series since last event

max_volume_since = max_since(breakout, volume)

min_since(...)

Minimum value of series since last event

min_volume_since = min_since(breakout, volume)

accumulate_while(...)

Accumulate value while condition is true

running_total = accumulate_while(volume > 1000, volume)

flag_once(...)

Flag that becomes true once and stays true

entry_flag = flag_once(signal_entry)

๐Ÿ”€Conditionals

Use if-expressions for conditional logic - choose values based on conditions:

# If-expression syntax
signal = if condition then value_if_true else value_if_false

# Example
is_bullish = close > open
signal_strength = if is_bullish then 1 else -1

# Multiple conditions
trend = if ma20 > ma50 then "up" else "down"

# Complex expressions
significant = if volume > 1000 then high else low

The if ... then ... else syntax allows you to conditionally select values based on boolean expressions.

โš™๏ธInput Parameters

Syntax detail: input at top of script.

Define configurable parameters at the top of your script - change them without modifying code!

input period = 20
input threshold = 0.5 as float
input use_sessions = true as bool

ma = sma(close, period)
breakout = high > high[1] * (1 + threshold)
output table

๐Ÿ’ก Tip: Inputs are constants during execution. You can optionally specify types with as float, as int, or as bool.

๐Ÿ”งUser-Defined Functions

Define reusable functions inline - make your code more organized and maintainable:

# Define a function
func double(x: float) -> float = x * 2
func is_positive(x: float) -> bool = x > 0

# Use the function
doubled_close = double(close)
is_bullish = is_positive(close - open)

output table

๐Ÿ“Œ Function Syntax

  • Function name must start with a letter or underscore
  • Parameters require explicit types (float, int, bool, string)
  • Return type must be specified with -> type
  • Functions are pure (no side effects) and are expanded inline in the generated SQL

๐Ÿ“ŠAggregations

Calculate statistics grouped by columns or expressions - powerful analysis made simple!

breakout = high > high[1] * 1.005

# Group by single column
stats by session:
  count(breakout) as total_breakouts
  percent(breakout) as breakout_probability
  avg(high - low) as avg_range

# Group by multiple columns
stats by session, interval_id:
  count(breakout)
  percent(breakout)

# Group by expressions (e.g., weekday)
stats by session, weekday(date):
  count(breakout_up) as breakout_up_count
  count(breakout_down)
  percent(breakout_up) as breakout_up_probability
  avg(2 * avg_volume) as double_avg_volume

output stats

count()

Count where condition is true

count(breakout)

percent()

Percentage where condition is true (0-100)

percent(breakout)

avg()

Average value (supports numeric and boolean)

avg(high - low)

max()

Maximum value in group

max(close)

min()

Minimum value in group

min(low)

stddev()

Standard deviation

stddev(close)

median()

Median value

median(close)

๐Ÿ’ก Key Features

  • Use as alias_name to customize column names
  • Group by expressions like weekday(date) for advanced analysis
  • avg(), max(), min(), stddev(), and median() accept numeric expressions
  • Boolean expressions are automatically converted to 0/1 for numeric aggregations
  • Use as alias_name to customize column names for all aggregation functions

๐Ÿ“คOutput

Specify what to output at the end of your script:

๐Ÿ“‹ Table Output

output table

Shows all calculated variables as columns - perfect for exploring your data!

๐Ÿ“Š Stats Output

output stats

Shows aggregated statistics - requires a stats by block

๐Ÿšฆ Signals Output

output signals

Shows only rows where signals are active - perfect for trading signals!

โฐTime-Based Analysis with interval_index & interval_number

interval_index is a global sequential index that continues summing across dates and sessions (e.g., 1010, 1011, 1012...).interval_number follows the same sequence but resets at the start of each session (1, 2, 3, 4, 5... per session). Both depend on interval_duration configured in your dataset metadata.

๐Ÿ• How They Work:

interval_index (Global):
โ€ข Continues summing: 1010, 1011, 1012...
โ€ข Does NOT reset by session or date
โ€ข Example: NY session 9:30-11:00 โ†’ interval_index = 1011
โ€ข Next session continues: 1012, 1013...
interval_number (Per Session):
โ€ข Follows interval_index sequence
โ€ข Resets to 1 at session start
โ€ข Example: NY session 9:30-11:00 โ†’ interval_number = 1
โ€ข Next interval 11:00-1:30 โ†’ interval_number = 2
โ€ข New session โ†’ resets to 1 again
Example with 90min intervals (NY session 9:30am-5:00pm):
9:30-11:00: interval_number = 1, interval_index = 1011
11:00-1:30: interval_number = 2, interval_index = 1012
1:30-3:00: interval_number = 3, interval_index = 1013
3:00-4:30: interval_number = 4, interval_index = 1014
4:30-5:00: interval_number = 5, interval_index = 1015 (partial interval)

Key insight: interval_index is global for cross-session analysis, while interval_number is local to each session for intra-session patterns.

๐Ÿ“Š Time Interval Analysis

# Compare behavior at the same TIME position within each session
# interval_number = 1 means "first bar of each time interval" for all sessions
stats by session, interval_index, interval_number:
  count(close > open) as bullish_bars
  avg(high - low) as avg_range
  percent(volume > avg(volume, 20)) as high_volume

output stats

# Example: Compare first 30min behavior across all sessions

๐ŸŽฏ Opening Range Analysis

# Focus on first time interval within each session
# interval_number = 1 means first interval of each session
stats by session where interval_number = 1:
  count(high > high[1]) as breakouts
  avg(high - low) as avg_range
  max(high - low) as max_range

output stats

# Perfect for analyzing opening range behavior
# Note: interval_index is global, use interval_number for session-relative analysis

๐Ÿ’ก When to use interval_index & interval_number:

  • โ€ข Opening Range Analysis: Use interval_number = 1 to compare first interval across sessions
  • โ€ข Global Sequential Analysis: Use interval_index for analysis that spans across dates and sessions
  • โ€ข Intra-session Patterns: Use interval_number to analyze behavior within each session (1st, 2nd, 3rd interval...)
  • โ€ข Time-based Breakouts: When breakouts happen by interval position within sessions
  • โ€ข Volume Distribution: Activity patterns by time intervals within sessions
  • โ€ข Custom Time Analysis: Configure interval_duration in dataset metadata (e.g., 60min, 90min)

๐ŸšฆSignals

Signals are boolean expressions that identify specific trading conditions. They're perfect for detecting entry/exit points, breakouts, and other trading opportunities.

โœจExplicit Signal Declaration

The clearest way to declare signals is using the signal keyword:

# Declare signals explicitly
signal entry = crossover(sma(close, 12), sma(close, 26))
signal exit = crossunder(sma(close, 12), sma(close, 26))

# Signals with descriptive names
signal golden_cross = ma_fast > ma_slow and ma_fast[1] <= ma_slow[1]
signal price_breakout = high > high[1]

output signals

โœ… Advantages

  • Maximum clarity: Makes it explicit that it's a trading signal
  • Flexible naming: No need to include "signal" in the name
  • Priority: Explicit signals have priority over automatic detection
  • Better readability: More expressive and easier to understand

๐ŸŽฏTypes of Signals

โœ… Boolean Signals

The most common type - simple true/false values:

signal buy = close > sma(close, 20)
signal sell = close < sma(close, 20)
signal breakout = high > high[1]

๐Ÿ“ Categorical Signals

Use conditionals to create categorical signals:

signal = if close > ma20 then "buy"
         else if close < ma20 then "sell"
         else "hold"

๐Ÿ”ข Numeric Signals

Can be numeric (0/1 or strength values):

signal = if close > ma20 then 1 else 0
signal_strength = if close > ma20 then 100 else 0

๐Ÿ”Implicit Signal Detection

Signals can also be detected automatically if they meet certain criteria:

๐Ÿ“Œ Automatic Detection Criteria

  • Variables of type bool or series<bool>
  • Variable names containing "signal" or ending in _signal
# These are automatically detected as signals
buy_signal = close > sma(close, 20)  # Contains "signal" in name
is_above_ma = close > ma              # Type is bool
breakout_detected = high > high[1]    # Type is bool

output signals

๐Ÿ“คOutput Signals

When you use output signals, the system:

  • Identifies signal variables using explicit declarations, type checking, and naming conventions
  • Filters rows: Only includes rows where at least one signal is true
  • Optimizes: Uses efficient SQL WHERE clauses
# Multiple signals
signal entry = crossover(ma_fast, ma_slow)
signal exit = crossunder(ma_fast, ma_slow)
breakout = high > high[1]  # Also detected as signal (type bool)

# Output only rows where at least one signal is true
output signals

๐Ÿ’กBest Practices

1๏ธโƒฃ Use Descriptive Names

โœ… Good

signal buy = ... signal sell = ... signal breakout = ...

โŒ Avoid

signal s = ... signal sig = ... signal x = ...

2๏ธโƒฃ Create Atomic Signals

Break down complex conditions into atomic signals and combine them:

โœ… Good: Atomic signals

above_ma = close > ma
high_volume = volume > 1000
breakout = high > high[1]

signal strong_entry = 
  above_ma and 
  high_volume and 
  breakout

โŒ Avoid: Monolithic

signal strong_entry = 
  close > ma and 
  volume > 1000 and 
  high > high[1]

3๏ธโƒฃ Multi-Condition Strategy

input ma_period = 20
input atr_period = 14
input volatility_threshold = 50

ma = sma(close, ma_period)
atr14 = atr(atr_period)

above_ma = close > ma
high_volatility = atr14 > volatility_threshold

signal strong_buy = above_ma and high_volatility
signal moderate_buy = above_ma and not high_volatility

output signals

๐ŸŽฏ Complete Example

# Golden Cross Strategy with Signals
input fast_period = 12
input slow_period = 26

ma_fast = sma(close, fast_period)
ma_slow = sma(close, slow_period)

# Explicit signals
signal entry = crossover(ma_fast, ma_slow)
signal exit = crossunder(ma_fast, ma_slow)

# Additional condition (auto-detected as signal)
above_ma = close > ma_fast

# Output only rows where signals are active
output signals

โš™๏ธConfig block & Backtesting

You can define backtest configuration (and optional script metadata) in two ways: in the script with a config block, or in the app using the graphical interface. The config block uses YAML syntax, starts with config: at the beginning of a line, and is optionalโ€”you can instead edit all backtest settings (signals, stop loss, take profit, capital, timeframes, etc.) in the app's UI.

Script or UI: You can write the config in the script (config block) for a self-contained strategy, or use the graphical interface in the app to edit backtest configuration without touching the script. Settings from the UI override or complement the config block when running a backtest.

Config block syntax

  • Place config: at the start of a line (usually at the top of the file).
  • Indent the following lines with spaces (YAML). The block ends when indentation returns to the base level.
  • The block is stripped before compilation; only DuckScript code is compiled to SQL.
config:
  backtest:
    signals:
      entry:
        long: ["fast_breakout"]
        short: []
      exit:
        long: ["exit_signal"]
        short: []
    initialCapital: 10000
    positionSize: 1

# Your DuckScript below (signals must match names in config)
signal fast_breakout = high > high[1]
signal exit_signal = close < sma(close, 20)
output signals

Backtesting config (under config.backtest)

When you run a backtest, the engine uses the config block (or UI/defaults). Under backtest: you can set:

FieldRequiredDescription
signals.entry.long / shortYesArrays of signal names for long/short entry
signals.exit.long / shortYesArrays of signal names for long/short exit
stop_lossYesObject with long/short: default_based_on (atr | price | percent), use_signal_level, default_multiplier or signal_name
take_profitYesSame structure as stop_loss
initialCapitalYesPositive number (e.g. 10000)
positionSizeOptionalPositive number (default 1)
signalTimeframeOptionalTimeframe for signals: 5min, 1h, 1d, etc.
executionTimeframeOptionalTimeframe for execution (e.g. 1min). Should be โ‰ค signalTimeframe.
position_managementOptionalmax_positions, allow_multiple_entries, close_on_opposite_signal

Signal names in the config must match signal names in your script. At least one entry signal (long or short) is required. If you omit the config block, backtest settings come from the app (graphical interface) or saved metadata.

Tip: Use the config block when you want the strategy to be self-contained and easy to share or version. Use the graphical interface when you prefer to edit signals, SL/TP, capital, and timeframes in the app without editing the script. The config block is extracted before compilation and does not affect the generated SQL.

๐ŸŽฏComplete Example

Here's a complete script that demonstrates many DSL (DuckScript) features:

# Golden Cross Strategy Analysis
input fast_period = 10
input slow_period = 20

# Calculate moving averages
ma_fast = sma(close, fast_period)
ma_slow = sma(close, slow_period)

# Detect crossovers
golden_cross = crossover(ma_fast, ma_slow)
death_cross = crossunder(ma_fast, ma_slow)

# Calculate statistics by session
stats by session:
  count(golden_cross) as total_golden_crosses
  count(death_cross) as total_death_crosses
  percent(golden_cross) as golden_cross_probability

# Output results
output stats

๐Ÿ’ญDSL (DuckScript) Philosophy

DuckScript is a pattern language, not a sequential simulation.

Instead of thinking โ€œbar by bar, what happens?โ€, think in patterns:

  • โ€œHow many consecutive bars?โ€
  • โ€œHow many bars since X?โ€
  • โ€œWhat's the maximum since Y?โ€

These patterns translate directly to efficient, scalable SQL.

โŒWhat You Cannot Do

Recurrence and State

QuantQuack DSL (DuckScript) does not allow variables to reference themselves:

# โŒ This is NOT allowed
x = x[1] + 1

# โŒ This too
cnt = condition ? cnt[1] + 1 : 0

# โŒ Circular dependencies
a = b + 1
b = a + 1

๐Ÿค” Why is this not allowed?

Recurrence breaks the deterministic and scalable model of the DSL (DuckScript). Here's why:

  • Determinism: Same code + same data should always produce the same results. Recurrence can create dependencies on evaluation order.
  • Scalability: SQL can parallelize and optimize pattern operations. With recurrence, the engine must execute bar-by-bar, which is slow on large datasets.
  • Clarity: Pattern functions express what you want, not how to do it step-by-step.
  • Performance: Built-in functions are optimized to run efficiently over millions of bars using vectorized SQL.

๐Ÿ’ก What you're really trying to do

When you write code like x = x[1] + 1 in Pine Script, you're usually trying to do one of these things:

1๏ธโƒฃ Count Consecutive Bars

What you want: โ€œHow many consecutive bars meet this condition?โ€

โŒ Pine Script way (with recurrence):

up = close > close[1]
cnt = up ? nz(cnt[1]) + 1 : 0

โœ… QuantQuack DSL (DuckScript) way:

up = close > close[1]
cnt = count_consecutive(up)

2๏ธโƒฃ Bars Since an Event

What you want: โ€œHow many bars have passed since this event occurred?โ€

โŒ Pine Script way (with recurrence):

breakout = high > high[1]
bars_since = breakout ? 0 : bars_since[1] + 1

โœ… QuantQuack DSL (DuckScript) way:

breakout = high > high[1]
bars_since = bars_since(breakout)

3๏ธโƒฃ Maximum/Minimum Since an Event

What you want: โ€œWhat's the maximum since this signal occurred?โ€

โŒ Pine Script way (with recurrence):

signal = crossover(ma_fast, ma_slow)
max_high = signal ? high : max(max_high[1], high)

โœ… QuantQuack DSL (DuckScript) way:

signal = crossover(ma_fast, ma_slow)
max_high = high_since(signal)

4๏ธโƒฃ Resettable Accumulators

What you want: โ€œAccumulate a value until a condition fails, then resetโ€

โŒ Pine Script way (with recurrence):

sum = condition ? sum[1] + value : 0

โœ… QuantQuack DSL (DuckScript) way:

sum = accumulate_while(condition, value)

๐ŸŽฏ Available Pattern Functions

Use these built-in functions instead of recurrence:

Count consecutive bars where condition is true

bars_since(condition)

Bars since last event

high_since(condition)

Maximum high since last event

low_since(condition)

Minimum low since last event

max_since(condition, series)

Maximum of any series since last event

min_since(condition, series)

Minimum of any series since last event

accumulate_while(condition, value)

Accumulate while condition is true

flag_once(condition)

Flag that activates once and stays active

highest(series, period)

Maximum value in a rolling window

lowest(series, period)

Minimum value in a rolling window

weekday(date)

Day of week (0=Sunday, 1=Monday, ..., 6=Saturday)

๐Ÿ’ญ DSL (DuckScript) Philosophy

DuckScript is a pattern language, not a sequential simulation.

Instead of thinking โ€œbar by bar, what happens?โ€, think in patterns:

  • โ€œHow many consecutive bars?โ€
  • โ€œHow many bars since X?โ€
  • โ€œWhat's the maximum since Y?โ€

These patterns translate directly to efficient, scalable SQL.