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 pricehighHighest pricelowLowest pricecloseClosing pricevolumeTrading volume๐ Metadata
timestampUnix timestampsymbolSymbol 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 identifierinterval_startInterval start timestampinterval_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 ANDorLogical ORnotLogical 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, -10Integer numbers
float3.14, -0.5Floating-point numbers
booltrue, falseBoolean 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:
timestampseries<float>Series of floats
Example:
close, highseries<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
truefalse# 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
๐ 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โ23minute(timestamp) โ 0โ59month(date) โ 1โ12day_of_month(date) โ 1โ31year(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_nameto customize column names - Group by expressions like
weekday(date)for advanced analysis avg(),max(),min(),stddev(), andmedian()accept numeric expressions- Boolean expressions are automatically converted to 0/1 for numeric aggregations
- Use
as alias_nameto customize column names for all aggregation functions
๐คOutput
Specify what to output at the end of your script:
๐ Table Output
output tableShows all calculated variables as columns - perfect for exploring your data!
๐ Stats Output
output statsShows aggregated statistics - requires a stats by block
๐ฆ Signals Output
output signalsShows 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:
โข 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...
โข 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
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 = 1to compare first interval across sessions - โข Global Sequential Analysis: Use
interval_indexfor analysis that spans across dates and sessions - โข Intra-session Patterns: Use
interval_numberto 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_durationin 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
boolorseries<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
WHEREclauses
# 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.
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 signalsBacktesting config (under config.backtest)
When you run a backtest, the engine uses the config block (or UI/defaults). Under backtest: you can set:
| Field | Required | Description |
|---|---|---|
signals.entry.long / short | Yes | Arrays of signal names for long/short entry |
signals.exit.long / short | Yes | Arrays of signal names for long/short exit |
stop_loss | Yes | Object with long/short: default_based_on (atr | price | percent), use_signal_level, default_multiplier or signal_name |
take_profit | Yes | Same structure as stop_loss |
initialCapital | Yes | Positive number (e.g. 10000) |
positionSize | Optional | Positive number (default 1) |
signalTimeframe | Optional | Timeframe for signals: 5min, 1h, 1d, etc. |
executionTimeframe | Optional | Timeframe for execution (e.g. 1min). Should be โค signalTimeframe. |
position_management | Optional | max_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.
๐ฏ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(condition)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.