Manual · 2 of 5
Query language
and. Everything else — looking back a few bars, counting streaks, averaging a window, picking dates — is a handful of prefixes and functions listed on this page. Every example was run against the live data on 7 Sep 2026; the numbers quoted are what came back.Anatomy of a query
In the app you normally type only a condition, starting with #. The leading # tells TPSL “this is a query, not a question”, and the app adds its own display columns (prices, returns, the core indicators) in front of it.
#bn_spot_rsi<=30 and entity=BTCTry this →
A complete query names its own columns before an @. TPSL runs it as written and shows the raw table with exactly those columns. Use this form for summary queries (see Summary queries) or when you want a specific column set.
date,entity,bn_spot_close,bn_spot_rsi@bn_spot_rsi<=30 and entity=BTC and date>=today-7Try this →
- Spaces are optional:
bn_spot_rsi <= 30andbn_spot_rsi<=30are the same. - Keywords are lowercase:
and,or,not,in,is.ANDis a syntax error. - Column names are lowercase with underscores; values are case-sensitive (
BTC,Monday). - One row = one coin on one 15-minute bar. A query without an
entitycondition returns both coins (the app addsentity=BTCto a#condition unless you mention ETH).
Columns and feed prefixes
Every price and indicator column is named <feed>_<indicator>: bn_spot_rsi is the RSI on Binance spot, okx_perp_rsi the same indicator on the OKX perpetual. All seven feeds carry the same 110 indicators. The Market / Exchange dropdowns on the result page decide which prefix the app uses for its display columns; in a condition you may mix prefixes freely.
| Prefix | Exchange | Market | Currency | Notes |
|---|---|---|---|---|
bn_spot | Binance | Spot | USDT | The default feed. Only Binance feeds carry taker-buy and trade-count columns. |
bn_perp | Binance | Futures (USDT-margined perpetual) | USDT | Taker-buy and trade-count columns available. |
okx_spot | OKX | Spot | USDT | |
okx_perp | OKX | Futures (perpetual swap) | USDT | |
cb_spot | Coinbase | Spot | USD | No quote_volume column. |
up_spot | Upbit | Spot | KRW | Prices are Korean won. 100000000 means ₩100,000,000. |
up_krwusdt | Upbit (internal) | KRW/USDT rate | KRW | Not selectable in the app; powers x_krw_premium. History starts 2024-06-07. |
Columns without a prefix
date,entity,day,day_num,month,year,hour_progress,day_progress,month_progress,year_progress— identity and calendar.x_binance_basis,x_okx_basis,x_krw_premium— cross-market columns (futures basis in %, kimchi premium in %).
The {P} placeholder
Write {P} instead of a prefix and the app substitutes the feed currently selected in the dropdowns. #{P}_rsi<=30 runs as bn_spot_rsi<=30 on Binance spot and as up_spot_rsi<=30 after switching to Upbit.
Where the names are
Comparisons, logic and arithmetic
| Operator | Meaning | Example |
|---|---|---|
= | equal (== also works) | entity=BTC · bn_spot_rsi=50 |
!= | not equal | date!=20260901 |
< <= > >= | less / greater than | bn_spot_rsi<=30 |
a<x<b | chained range | 30<bn_spot_rsi<40 |
in (…) | exact membership (not a range) | day in ('Saturday','Sunday') |
not in (…) | exclusion | date not in (20260901,20260902) |
is None / is not None | the value is missing / present | bn_spot_adaptive_dma is None |
and | both conditions | bn_spot_rsi<=30 and bn_spot_mfi<=20 |
or | either — always inside parentheses | (bn_spot_rsi<30 or bn_spot_mfi<20) and bn_spot_adx<20 |
not | negate the next comparison | not bn_spot_rsi>30 |
+ − * / ** % // | arithmetic, power, remainder, integer division | bn_spot_close/p96:bn_spot_close>=1.05 |
(…) | grouping and precedence | (bn_spot_close/A(bn_spot_close@entity,N=96)-1)*100<-2 |
Values
- Numbers as written:
0.95,70000,-2. Percent columns are already in percent, so “down 2 %” isbn_spot_ret_log_short<=-2, not-0.02. - Ratios from joins are plain ratios: “up 5 %” is
bn_spot_close/p96:bn_spot_close>=1.05. - Text values:
BTC,ETHand weekday names may be written bare or quoted (entity=BTC,day='Monday'). Anything else must be quoted. in (…)is exact membership:bn_spot_rsi in (30,40)means RSI is exactly 30 or exactly 40. For a range use30<bn_spot_rsi<40.
Null never matches
A missing value (warm-up period, an exchange gap, no event yet, a future bar that has not happened) compares as false with every operator. You do not need null guards in a condition — a bar without an RSI simply never satisfies bn_spot_rsi<=30. To find the missing bars themselves, use is None.
S(1)@entity=BTC and bn_spot_adaptive_dma is NoneTry this →
Put every or inside parentheses
or splits the WHOLE condition in two: entity=BTC and date=20260901 and bn_spot_rsi<=30 or bn_spot_mfi<=20 means “(all of the first three) or (mfi ≤ 20 on any coin, any day)” — the verification run returned 18,661 rows including ETH. Write (bn_spot_rsi<=30 or bn_spot_mfi<=20) instead.Looking back and forward: p:, n:, o:
A prefix before a column name reads the same column on another row of the same coin. Bars are 15 minutes, so the count is easy to convert.
| Prefix | Reads | Example |
|---|---|---|
p:X | X one bar earlier (15 minutes) | p:bn_spot_close |
pN:X | X N bars earlier | p4:bn_spot_close (1 h) · p96:bn_spot_close (1 day) · p672: (7 days) |
nN:X | X N bars LATER — null while that bar does not exist yet | n96:bn_spot_close/bn_spot_close (next-day ratio) |
o:X | the OTHER coin's X at the same time | on a BTC row, o:bn_spot_close is ETH's close |
po:X · op:X | the other coin's X one bar earlier | po:bn_spot_ret_log |
t:X | the row's own X (the implicit default) | t:bn_spot_close = bn_spot_close |
| 1 bar | 4 bars | 16 bars | 32 bars | 48 bars | 96 bars | 192 bars | 672 bars |
|---|---|---|---|---|---|---|---|
| 15 minutes | 1 hour | 4 hours | 8 hours | 12 hours | 1 day | 2 days | 7 days |
#bn_spot_close/p96:bn_spot_close>=1.05 and entity=BTCTry this →
#bn_spot_ret_log_short>o:bn_spot_ret_log_short and entity=BTCTry this →
What p: means on an indicator (read this once)
Indicators are stored with a one-bar lag: on any row, bn_spot_rsi is the RSI as of the previous completed bar — the last value you could have known when this bar opened. So p:bn_spot_rsi is the RSI as of two bars back, and “RSI rose over the last hour” is bn_spot_rsi>p4:bn_spot_rsi. Raw candle columns (open, high, low, close, volume) are the row's own bar, so p:bn_spot_close is genuinely the previous close. The Data & integrity page explains why.
Edges and gaps
p:is null on the first bar of the history (1 Jan 2020 00:00 UTC) andnN:is null on the last N bars — the future has not happened. Null never matches, so such rows drop out of the condition silently.p:means “the previous stored bar”. Exchange maintenance gaps are rare (Binance spot has 154 missing bars in six and a half years) but exist;CryptoSlot(date)-CryptoSlot(p:date)=1is true only when the previous bar is really 15 minutes earlier.- Prefixes cannot be written with two colons:
p:o:xfails,po:xworks.
Window functions: Sum, A, Max, Min
A window function looks at the previous N bars of the same coin — the current bar is not included — and returns one number. The shape is always Function(expression@entity,N=k).
| Function | Numeric expression | Boolean expression (a comparison) |
|---|---|---|
Sum(x@entity,N=k) | total of the previous k values | how many of the previous k bars were true |
A(x@entity,N=k) | average (moving average) — Average also works | share of true bars, 0 to 1 |
Max(x@entity,N=k) | highest of the previous k values | — |
Min(x@entity,N=k) | lowest of the previous k values | — |
#Sum(bn_spot_close>bn_spot_open@entity,N=8)>=6 and entity=BTCTry this →
#A(bn_spot_rsi@entity,N=16)<40 and bn_spot_rsi>p:bn_spot_rsi and entity=BTCTry this →
#bn_spot_close>Max(p:bn_spot_high@entity,N=96) and entity=BTCTry this →
#bn_spot_close>Max(p:bn_spot_high@entity,N=672) and entity=BTCTry this →
#(bn_spot_close/A(bn_spot_close@entity,N=96)-1)*100<-2 and entity=BTCTry this →
- Before the window has k values,
Sumreturns 0 and the others return null (so a comparison is false). - Null values inside the window are skipped, not counted as 0.
- The
@entitypart is required: it tells the engine to keep BTC and ETH windows separate. - Choose N from the bar table: 4 = 1 h, 16 = 4 h, 96 = 1 day, 672 = 7 days.
Streak()
Streak(expression@entity) counts how many consecutive bars, ending with the previous bar, had the same sign. It is the tool for “N in a row”.
| Expression value on each bar | Effect on the streak |
|---|---|
| positive (or a true comparison) | +1, +2, +3 … while it stays positive |
| negative (or a false comparison) | −1, −2, −3 … while it stays negative |
| zero or missing | reset to 0 |
| sign flips | restarts at +1 or −1 |
| Bar | close − open | Streak() read on the NEXT bar |
|---|---|---|
| bar 1 | +0.3 | +1 |
| bar 2 | +0.1 | +2 |
| bar 3 | −0.4 | −1 |
| bar 4 | −0.2 | −2 |
| bar 5 | −0.5 | −3 |
| bar 6 | 0 | 0 (reset) |
| bar 7 | +0.2 | +1 |
#Streak(bn_spot_close-bn_spot_open@entity)>=6 and entity=BTCTry this →
#Streak(bn_spot_close-bn_spot_open@entity)<=-6 and entity=BTCTry this →
#Streak(bn_spot_rsi>=70@entity)>=10 and entity=BTCTry this →
- Combine with the current bar to say what happened next:
Streak(bn_spot_close-bn_spot_open@entity)>=6 and bn_spot_close<bn_spot_open= the first red bar after six greens. - A numeric expression follows its sign; a comparison is +1 for true and −1 for false.
Streak(bn_spot_ret_log@entity)andStreak(bn_spot_ret_log>0@entity)agree except on exact zeros. - The pre-computed columns
positive_return_streakandtrend_streakstore ln(1 + run) for two fixed definitions;Streak()works for any expression.
Dates, today and the calendar
date is a number in the form YYYYMMDD.HHMM, the UTC start time of the bar. The bar that opened at 09:30 UTC on 1 September 2026 is 20260901.0930. Results print it as 20260901.093 because trailing zeros are dropped — .093 is 09:30, .13 is 13:00, .0 is midnight.
| Write | Selects | Note |
|---|---|---|
date=20260901 | every bar of that day (96 rows) | a whole-day equality expands to the day |
date=20260901.0930 | exactly the 09:30 UTC bar | one row |
date>=20260901 and date<20260902 | a range | the same day, written as a range |
date in (20260901,20260902) | several whole days | 192 rows |
date in (20240918.1800,20241107.1900) | several exact bars | 2 rows |
date!=20260901 | everything except that day | |
date=today | the current UTC day so far | today is the UTC calendar day |
date=today-1 | yesterday (UTC) | 96 rows |
date>=today-7 | from midnight seven days ago | 725 rows at 13:00 UTC on the day of verification |
Two date pitfalls
(date=20260901 or date=20260902) matches only the two midnight bars. Write date in (20260901,20260902) instead. And today is the UTC calendar day: at 08:00 in Seoul, date=today already means the day that started at 09:00 KST.Calendar columns
Prefer these over arithmetic on date. They come from the bar's own UTC start time.
| Column | Values | Example |
|---|---|---|
day | 'Monday' … 'Sunday' (quoted) | day in ('Saturday','Sunday') |
day_num | 1–31 | day_num=25 |
month | 1–12 | month=12 |
year | 2020 … | year>=2024 |
hour_progress | 0 / 25 / 50 / 75 — the quarter of the hour | hour_progress=0 |
day_progress | 0 … 98.96 — minutes since midnight ÷ 14.4 | day_progress=0 |
month_progress | 0 … <100 — position inside the month | month_progress<5 |
year_progress | 0 … <100 — position inside the year | year_progress>=95 |
A specific time of day
day_progress = minutes since 00:00 UTC ÷ 14.4. Each bar adds 1.0417, so a window of ±0.5 around the value catches exactly one bar per day. 09:30 → 570 ÷ 14.4 = 39.58 → day_progress>=39.08 and day_progress<40.08. Only midnight can be written with = (day_progress=0).
| 00:00 UTC | 04:00 UTC | 08:00 UTC | 09:30 UTC | 12:00 UTC | 13:30 UTC | 14:30 UTC | 16:00 UTC | 20:00 UTC | 21:00 UTC | 23:45 UTC |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 16.67 | 33.33 | 39.58 | 50 | 56.25 | 60.42 | 66.67 | 83.33 | 87.5 | 98.96 |
#day_progress>=56 and day_progress<57 and entity=BTCTry this →
One row per day
Any “on days when …” question wants one row per day. Add day_progress=0 so only the midnight bar is returned.
#month=12 and day_num=25 and day_progress=0 and entity=BTCTry this →
Summary queries and groups
Put an aggregate in the column part of a complete query and the answer collapses to a single row: how many bars matched, what happened on average afterwards, the best and worst outcome. This is the fastest way to size up a setup before opening the full table.
| In the column list | Returns |
|---|---|
S(1) | the number of matching bars |
A(x) | the average of x over the matching bars (Average also works); for a comparison, the share of true bars |
Sum(x) | the total; for a comparison, the count of true bars |
Max(x) · Min(x) | the extremes |
U(x) | the distinct values of x |
S(1),A(n96:bn_spot_close/bn_spot_close)@bn_spot_rsi<=30 and entity=BTCTry this →
S(1),Sum(bn_spot_close>bn_spot_open),Average(n4:bn_spot_close/bn_spot_close),Max(n96:bn_spot_close/bn_spot_close),Min(n96:bn_spot_close/bn_spot_close)@entity=BTC and bn_spot_rsi<=20Try this →
S(1),A(n96:bn_spot_close/bn_spot_close)@entity and bn_spot_rsi<=30Try this →
S(1),A(bn_spot_close/bn_spot_open>=1),A(n96:bn_spot_close/bn_spot_close)@entity=BTC and day='Monday' and day_progress=0Try this →
Groups
- A bare column name in the condition groups by its values:
…@entity and bn_spot_rsi<=30gives one summary row for BTC and one for ETH. - A comma after a value fans one condition out into several groups:
bn_spot_rsi<=30,25,20runs three thresholds at once,entity=BTC,ETHboth coins. The result page shows one table per group. - Inside a column list, a window function with
N=is not evaluated per row — it reports the state after the last matching row. KeepN=windows in the condition and use plain aggregates in the column list.
#bn_spot_rsi<=30,25,20 and entity=BTCTry this →
Aliases and helper functions
Naming a column
Add as name after a column expression. Wrap an arithmetic expression in parentheses first, otherwise the alias attaches only to the last term.
date,entity,(bn_spot_close/p96:bn_spot_close) as ret_1d@entity=BTC and date=todayTry this →
Functions you can use in an expression
abs(x),round(x),int(x),float(x),min(a,b,…),max(a,b,…),sum(list),len(list),sorted(list),str(x)— the usual Python helpers.math.log(x),math.sqrt(x)and the rest of the math module;x**0.5also works.CryptoSlot(date)— the bar's sequential 15-minute slot number;CryptoSlot(date)-CryptoSlot(p:date)=1checks that the previous bar is adjacent.- Any comparison is a value too:
(bn_spot_close>bn_spot_open)is 1 or 0, which is why it can go insideSum,AandStreak.
date,entity,max(p1:bn_spot_high,p2:bn_spot_high,p3:bn_spot_high,p4:bn_spot_high)/bn_spot_close as high_1h_vs_close@entity=BTC and date=todayTry this →
Lists of joins (advanced)
You can build a list from explicit references and count inside it, sports-style. It works, but Sum(…@entity,N=k) says the same thing more clearly and faster.
#len([x for x in (p1:bn_spot_ret_log,p2:bn_spot_ret_log,p3:bn_spot_ret_log,p4:bn_spot_ret_log) if x>0])>=3 and entity=BTC and date=todayTry this →
Sum(p:bn_spot_ret_log>0@entity,N=4)>=3.date (str(date)[-4:]) are unreliable because the number drops trailing zeros. Use the calendar columns instead.Limits and performance
- Typical response: 1–4 seconds for a condition over the full history; up to about 7 seconds with a 672-bar window; a plain-language question adds a translation step (a few seconds, cached afterwards).
- The app sends up to 8,000 characters of query; plain-language questions are limited to 2,000 characters and about 20 comparisons.
- Very large results (tens of thousands of bars) combined with many columns can exceed the engine's memory budget. The Result tab asks for fewer columns than the Data tab and the app splits the request automatically; narrowing the dates always helps.
- Long joins (
p672:) are accepted but expensive over the whole history; preferMax/Min/Sum(…,N=672). - Results are read-only snapshots of an append-only history: rerunning a query later returns the same rows plus the bars that arrived since (and
todaymoves).
Error messages and fixes
| What you see | Usual cause | Fix |
|---|---|---|
| The engine could not evaluate part of this query. Check column names and expressions. | A column name the engine does not know: a typo (bn_spot_rsii), the wrong feed prefix, a Binance-only column on another feed (okx_spot_taker_buy_ratio), or a lowercase value (entity=btc). | Copy names from the Indicator reference; values are case-sensitive: BTC, ETH, Monday. |
| SDQL syntax error near character N | An operator typo (<<), an uppercase keyword (AND, OR), an unbalanced parenthesis, or a stray comma. | Keywords are lowercase; count your parentheses; the character position points at the problem. |
| The engine could not run this query at this size (bars × columns). | The result is very large and the request asked for many columns, or for long joins over the whole history. | Add a date window, use the Result tab (fewer columns) or replace a long pN: join with a window function. |
| No rows matched this query | Every condition is true only for zero bars — often a sign mistake on a dev % column, a percent written as a ratio (0.02 instead of 2), or a null value. | Loosen one threshold at a time; remember sma<0 means price ABOVE the average; nulls never match. |
| That doesn't look like a query yet | Fewer than two readable characters. | Type a question, or a condition starting with #. |
| A notice instead of a table (plain-language questions) | The translator asked for clarification (data TPSL does not have, an ambiguous question), or the daily translation quota is used up. | Rephrase with the columns TPSL has, or write the condition yourself with #. |
Cheat sheet
Next: the Examples page groups fifty verified queries by goal.