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, summary, by, if/then/else, types. Donโ€™t use them as variable names (except signal).

inputfuncsignaloutputstatssummarybycountpercentavgasifthenelseintfloatboolstringtruefalse

๐Ÿ”ฃ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. Compute session/day boundaries with bar_size, session_end_timestamp, session_start_timestamp, date_end_timestamp, date_start_timestamp. Convert timezones with time_to_utc and date_from_epoch. Check for nulls with na(x). 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
bar_size(ts?) โ†’ bar duration
session_end_timestamp(session, ts?) โ†’ session end epoch
session_start_timestamp(session, ts?) โ†’ session start epoch
date_end_timestamp(date?) โ†’ end of day
date_start_timestamp(date?) โ†’ start of day
time_to_utc(timestamp) โ†’ UTC epoch
date_from_epoch(epoch) โ†’ YYYY-MM-DD
na(x) โ†’ true if null

๐Ÿ” 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)

pivothigh(...)

Swing high: source higher than leftBars and rightBars; returns value or na

ph = pivothigh(high, 2, 2)

pivotlow(...)

Swing low: source lower than leftBars and rightBars; returns value or na

pl = pivotlow(low, 2, 2)

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)

carry_while(...)

First value when condition became true, held during segment

dq3_open = carry_while(session == "DQ3", open)

carry_forward(...)

Last value when condition was true, propagated forward (LOCF)

last_dq3_close = carry_forward(session == "DQ3", close)

flag_once(...)

Flag that becomes true once and stays true

entry_flag = flag_once(signal_entry)

latch(...)

Flag that holds until reset (latch(condition) or latch(condition, reset))

lat = latch(signal, new_bar)

๐Ÿ”€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

# Optional: one row of aggregates over the stats by result
summary:
  count(breakout_up_count > 5) as high_breakout_sessions
  avg(breakout_up_probability) as avg_breakout_prob

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)

first()

Value from first row in group (by timestamp)

first(timestamp)

last()

Value from last row in group (by timestamp)

last(close)

first_when()

First value among rows where condition is true

first_when(open, session == "DQ3")

prev()

Value from previous group (requires column from same block)

prev(dq3_high)

lead()

Value from next group (requires column from same block)

lead(dq1_high, 1)

๐Ÿ’ก Key Features

  • Use as alias_name to customize column names
  • Group by expressions like weekday(date) for advanced analysis
  • avg(), max(), min(), stddev(), median(), first(), last(), and first_when() accept numeric expressions
  • prev(column) and lead(column, n) access previous or next group values (only in stats by, not in summary)
  • summary: after stats by produces one row of aggregates over the stats result (same functions except prev/lead)
  • 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. With summary:, returns one row of summary aggregates.

๐Ÿšฆ 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

carry_while(condition, value)

First value in segment; na when false

carry_forward(condition, value)

Last value propagated forward (LOCF)

flag_once(condition)

Flag that activates once and stays active

latch(condition)

Same as flag_once

latch(condition, reset)

Flag that holds until reset, then can trigger again

pivothigh(source, leftBars, rightBars)

Swing high: source > bars left and right

pivotlow(source, leftBars, rightBars)

Swing low: source < bars left and right

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)

Bar duration in timestamp units

session_end_timestamp(session, ts?)

Epoch of session end

Epoch of session start

Epoch end of day

Epoch start of day

time_to_utc(timestamp)

UTC epoch in seconds

Date string YYYY-MM-DD from epoch

na(x)

True if x is null (Pine Script compatible)

๐Ÿ’ญ 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.

โ“FAQ

Quick answers to the most common DuckScript questions. For full reference, see Built-in Functions and What You Cannot Do.

Why do I get "Circular dependency" or "Unknown function"?

DuckScript has no bar-by-bar state (no var, no bar_index). Use built-ins instead: count_consecutive, bars_since, accumulate_while, flag_once, latch(condition, reset), or session_end_timestamp + bar_size for "last bar of session".

What You Cannot Do

How do I detect the last bar of a session without lookahead?

Use the canonical one-liner below. You need config.sessions and config.timezone in your config block.

Code Snippets โ†’

Can I use expressions in input defaults?

No. Inputs must be literals only: input period = 20 โœ“ โ€” input period = n + 1 โœ—. Define a variable after inputs for any derived value.

What columns are available in my script?

Always: open, high, low, close, volume, timestamp, symbol. When the dataset has session/interval metadata: date, session, interval_id, interval_start, interval_index, interval_number.

Available Columns

How do I export to Pine Script?

Use export in the app or CLI. Pine Script export is for signals and strategies (e.g. signal long_entry = ..., output signals), not for statistics โ€” stats by and aggregations are not exported to Pine. Common built-ins (sma, crossover, session_end_timestamp, bar_size, etc.) map to Pine; only the helpers your script actually uses are included in the output.

I get "Undefined variable" โ€” what do I do?

Variables must be defined before use; order of lines matters. Check for typos. Inside a func, only the function parameters and global columns (e.g. close, timestamp) are in scope.

Can I use close[1] or only fixed indices?

Yes. Only the past with a literal index: close[1], high[2]. You cannot use close[n] with a variable index.

Why do I get "Type mismatch" in count() or percent()?

In stats by, count(expr) and percent(expr) require a boolean expression. Use e.g. count(close > open), not count(close).

Where does config go and how many can I have?

Exactly one config: block at the very beginning of the script, before any input or variable assignment.

What is the difference between output table, output stats, and output signals?

output table: every row with all calculated columns. output stats: only the result of stats by (and summary if present). output signals: only rows where at least one signal ... declaration is true.

"summary block requires a preceding stats by" โ€” what does that mean?

If you use summary:, it must come after a stats by block. In summary you reference only columns (aliases) from that stats by.

How do I count consecutive bars where a condition is true?

Use the built-in count_consecutive(condition). Do not try to maintain a bar-by-bar counter variable.

How do I backtest a strategy?

Configure config.backtesting (capital, signal_timeframe, execution_timeframe, position_size, risk_management) in your config block, declare named signals (e.g. signal long_entry = ...), and use output signals. The app or CLI runs the backtest over your dataset.

Config block & Backtesting

๐Ÿ“‹Code Snippets

Copy-paste ready patterns. Scripts using session_end_timestamp or session_start_timestamp need a config: block with timezone and sessions.

Session logic

Last bar of session (no lookahead)

Identifies the closing bar of each session using the session end timestamp and bar size. No need to look at the next row.

is_session_end_bar = (session_end_timestamp(session) - timestamp) >= 0 and (session_end_timestamp(session) - timestamp) <= bar_size()

Tip: use end_ts = session_end_timestamp(session) and bar = bar_size() to avoid repeating the calls.

Signals

Moving average crossover

Golden cross (fast crosses above slow) and death cross (fast crosses below slow).

fast = sma(close, 10)
slow = sma(close, 20)
golden_cross = crossover(fast, slow)
death_cross = crossunder(fast, slow)
Aggregations

Stats by session and date

Count signals, win rate, and average close per session and day. Requires structural columns session and date.

signal = close > sma(close, 20)
stats by session, date:
  count(signal) as signals
  percent(signal) as pct_up
  avg(close) as avg_close
output stats
Config

Sessions config (required for session_end_timestamp)

Place this at the top of your script when using session timestamps. Times are in the given timezone.

config:
  timezone: "America/New_York"
  sessions:
    - name: "regular"
      start_time: "09:30"
      end_time: "16:00"
    - name: "premarket"
      start_time: "04:00"
      end_time: "09:30"
Conditionals

If-then-else (no ternary)

Use if condition then a else b; the DSL does not support x ? a : b.

above_ma = close > sma(close, 20)
side = if above_ma then 1 else if close < sma(close, 20) then -1 else 0
Patterns

Consecutive bars and bars since

Count consecutive true bars or bars since the last time a condition was true. Use built-ins instead of bar-by-bar state.

consecutive_up = count_consecutive(close > open)
bars_since_breakout = bars_since(close > high[1])
Historical values

Previous bar (literal index only)

Access prior bars with a literal index: close[1], high[2]. Variable indices like close[n] are not allowed.

prev_close = close[1]
gap_up = open > close[1]
Indicators

RSI and ATR

Common technical indicators. Use with close (and period for RSI).

rsi14 = rsi(close, 14)
atr14 = atr(14)
overbought = rsi14 > 70
Signals

Named signals for export or backtest

Declare named signals for output signals or Pine/backtest. Each line is a boolean expression.

fast_ma = sma(close, 10)
slow_ma = sma(close, 20)
signal long_entry = crossover(fast_ma, slow_ma)
signal short_entry = crossunder(fast_ma, slow_ma)
output signals
Aggregations

Stats by date only (no sessions)

When you only have date (or no session column), aggregate per day. Requires a date column.

stats by date:
  count(*) as bars
  avg(close) as avg_close
  max(high) as day_high
output stats
Aggregations

First and last per group

In stats by, use first(expr) and last(expr) for the first or last value in each group (ordered by timestamp).

stats by session, date:
  first(timestamp) as first_ts
  last(close) as last_close
  count(*) as bars
output stats
Aggregations

Summary block (one row over stats)

After stats by, add summary: to aggregate over the stats result and get a single row. Reference only column aliases from the stats by.

stats by date:
  max(high) as day_high
  min(low) as day_low

summary:
  count(day_high > day_low) as up_days
  avg(day_high) as avg_high

output stats
Functions

User-defined func

Define inline functions with func name(params) โ†’ returnType = expression. Use them like built-ins. Only params and global columns are in scope inside the func.

func double(x: float) -> float = x * 2
func above(a: float, b: float) -> bool = a > b

doubled_range = double(high - low)
is_above_ma = above(close, sma(close, 20))
Null handling

na(x) and not na(x)

na(x) is true when x is null; not na(x) when x has a value. Use for null-safe logic and Pine Scriptโ€“compatible checks.

valid_close = not na(close)
score = if valid_close then close - open else 0
Minimal

Minimal valid script

Smallest script that compiles and runs: config with timezone and an output. Add variables and stats as needed.

config:
  timezone: "UTC"

range = high - low
output table