# TextToQuant full text TextToQuant turns a trading strategy written in plain English into an exact, deterministic set of rules, backtests it against real market history, and returns a graded report. It covers crypto, forex and equities, and every run reports its own honesty flags for zero fees, missing stops and too few trades, so a result that does not prove anything says so. Source: https://www.texttoquant.com Generated: 2026-10-08 --- # Documentation ## Documentation Source: https://docs.texttoquant.com Summary: What TextToQuant is, and how the pipeline turns a sentence into a backtest. TextToQuant turns a sentence into a backtested trading strategy. You describe an idea in plain English, and the platform parses it into a precise, deterministic strategy specification, runs it bar by bar against real market data, and hands you an honest report: performance, risk, and an overfitting verdict. These docs are organised by intent: **learn** the ideas, follow a **guide** to get a job done, or consult the **reference** for exact behaviour. ## How it works Write your idea the way you'd say it out loud: asset, signal, exits, timeframe. The query compiles deterministically into a fixed strategy spec. The AI never scores the result, so it can't steer toward a good looking number. A bar by bar engine simulates the strategy on real data, using only closed bar information (no look ahead). Metrics, chart, trade ledger, robustness checks and a letter grade, with everything you need to trust or reject the result. ## Start here Every performance metric, what it means, and how to read it honestly. Guided, hands on lessons across Foundations, Validation and Risk. ## Trade it When a strategy has earned your trust, deploy it. The same compiled strategy runs forward on live data, in **paper** with simulated fills, as **signal only** alerts, or **live** on your own exchange account, behind risk checks, exchange side stops and a kill switch. From a backtest to a running deployment, and the safety model behind every order. Mirror a leader's live strategy on your own account, sized to your own budget and limits. ## What makes it different - **Deterministic by construction.** Same query ⇒ same spec fingerprint ⇒ same test. Results are reproducible, and the header shows a Reproducible chip to prove it. - **Overfitting aware.** Beyond Sharpe and win rate, the report ships probabilistic and deflated statistics that correct for how many configurations you tried. - **No look ahead.** Signals read only closed bar data, so a backtest can't cheat with information it wouldn't have had live. Read the [Metrics reference](/docs/reference/metrics) next: understanding what the numbers mean is the fastest way to get value from every backtest you run. --- ## The pipeline Source: https://docs.texttoquant.com/start/pipeline Summary: Query → parse → indicators → backtest → report, in 60 seconds. Every backtest follows the same path: your sentence becomes a structured strategy, the strategy runs bar by bar against real data, and you get a report. Nothing is wired by hand, and nothing about the scoring is left to the AI. ## The five stages | Stage | What happens | | --- | --- | | Query | You describe the strategy in natural language | | Parsed | The query compiles deterministically into a structured spec (conditions, stops, sizing) | | Indicators | Every indicator the spec references is computed: engine built ins and any custom series | | Backtest | A bar by bar simulation opens and closes trades using only closed bar data | | Output | Metrics, an annotated chart, the trade ledger, robustness checks and a grade | ## What the engine does OHLC candles plus any market data the strategy needs (funding, open interest, cross asset series). Built in indicators and your custom series, aligned to the strategy's timeframe. Entry and exit trees are checked on each closed bar; stops and targets are tracked continuously. Positions open and close with realistic fills, producing trades, an equity curve and P&L. The AI parses your words but never scores the result. The same query compiles to the same spec fingerprint, which the same engine scores identically every time, so a run is reproducible, and the AI can't steer toward a good looking number. Next: learn the [shape of a query](/docs/start/query-shape), or jump to the [Metrics reference](/docs/reference/metrics) to read a report. --- ## Query shape Source: https://docs.texttoquant.com/start/query-shape Summary: The anatomy of a strategy sentence: direction, asset, conditions, exits, timeframe. You compose a strategy in plain words. The parser reads a handful of *slots* out of your sentence; fill the ones you care about and it will make reasonable assumptions for the rest (and show you what it assumed, so you can change it). ## The slots | Slot | What it sets | Example | | --- | --- | --- | | Direction | Long or short | `buy`, `short` | | Asset | The symbol(s) to test | `BTC`, `ETH` | | Entry | The condition that opens a trade | `when RSI crosses above 50` | | Exit | Conditions, stops and targets that close it | `exit below 50`, `SL 2%`, `TP 3RR` | | Timeframe | The bar size | `1h`, `4h`, `1d` | | Window | The date range to test | `last 90 days`, `from 1/1/2024 to 3/31/2024` | ## A worked example Take this sentence: ```text Buy BTC when RSI(14) crosses above 30, exit at 3R or a 2% stop, on the 4h for the last 90 days ``` The parser reads it as: | Slot | Value | | --- | --- | | Direction | Long (`Buy`) | | Asset | `BTCUSDT` | | Entry | `RSI(14) crosses above 30` | | Exit | Take profit at `3R`, stop loss at `2%` | | Timeframe | `4h` | | Window | Last 90 days | Anything the parser filled in for you is marked with a dashed amber chip. Click it to confirm or change the value before you spend a run. To go deeper, see the full [query syntax](/docs/reference/query-syntax), the [indicator library](/docs/reference/indicators), and the [operators](/docs/reference/operators) you can compare with. --- ## Write your first strategy Source: https://docs.texttoquant.com/guides/first-strategy Summary: Go from a plain English idea to a scored backtest. This guide takes you from a blank terminal to a graded backtest. You can build a strategy three ways: they all compile to the same engine, and you can switch between them at any time. ## 1. Describe the idea In the terminal, write the strategy the way you'd say it: direction, asset, entry, exit, timeframe, window. The parser fills in sensible defaults and marks them with amber chips so you can confirm or change them. Prefer no syntax? The visual builder is point and click condition trees: add indicator, price and pattern conditions, group them with AND / OR or run them in sequence, and set stops, sizing and direction. Every visual strategy reads back as text and runs on the same engine. The AI copilot (Pro and above) is a chat panel in the builder. Ask for a change ("add a 2% stop", "take profit at 2R", "switch to 4h") and it proposes a reviewable card you apply or dismiss. Nothing changes without your click. ## 2. Check what was parsed Open the parsed strategy card and confirm the pieces before you spend a run: asset, timeframe, direction, entry/exit conditions, stops and sizing. Edit anything inline: periods, thresholds, SL/TP, direction, then save. A query needs an asset, an entry, an exit, a timeframe and a window to run well. The completeness hints and the Rephrase tool nudge you toward a full query before you spend a backtest. ## 3. Run and read Hit run. In a few seconds you get summary metrics, an annotated chart, the trade ledger and a letter grade, plus the robustness checks on Power and above. Next: learn to [read the report](/docs/guides/read-a-report), then [validate the idea](/docs/guides/validate-an-idea) before you trust it. --- ## Read a backtest report Source: https://docs.texttoquant.com/guides/read-a-report Summary: How to interpret every panel of the results view. A results view has a lot on it. This guide is the reading order that stops you fooling yourself: risk before reward, sample size before everything. ## 1. Start with the risk, not the return The largest peak to trough equity drop is the pain you'd have had to sit through. Ask honestly: could you hold through it live? If not, the return doesn't matter. Under ~30 trades, treat every other number as noisy. A dazzling return on 6 trades is a coin flip, not an edge. Total return tells you the reward; Sharpe (or Sortino) tells you how much risk bought it. See the full definitions in the [Metrics reference](/docs/reference/metrics). ## 2. Read the trade quality Win rate alone is meaningless. Pair it with the payoff ratio. A 40% win rate with big winners can beat a 70% win rate with small ones. Expectancy (in R) is the single number that combines both. ## 3. Look at the chart and ledger The annotated chart shows entries and exits on price plus the equity and drawdown curves. The trade ledger lists every trade with its R multiple. Scan it for whether a handful of outliers carried the whole result. ## 4. Check the honesty signals The header shows a **Reproducible** chip (same query ⇒ same test) and, when a hold out split exists, an always on **out of sample** verdict. If the overfitting aware statistics are present, read them: a big Deflated Sharpe drop from the raw Sharpe means the result was mostly search luck. Use the letter grade to triage which runs deserve a closer look, then [validate the survivors](/docs/guides/validate-an-idea). Go deeper with the [Analysis & export reference](/docs/reference/analysis). --- ## Validate an idea Source: https://docs.texttoquant.com/guides/validate-an-idea Summary: Use robustness, out of sample and Monte Carlo to pressure test an edge. A good backtest is a *hypothesis*, not a conclusion. This guide is the workflow that separates a real edge from a lucky fit. Run it on any strategy before you believe the headline number. Walk forward, parameter sweeps, the robustness panel and the overfit verdict are on Power and above. Monte Carlo is on Pro and above. ## 1. Does it hold out of sample? When a hold out split exists, the header shows an always on OOS verdict: performance on data the strategy was never fit on. If it collapses out of sample, stop here: the edge was in the fitting. Walk forward refits parameters on rolling in sample windows and grades them only on the forward window that follows: the strictest test, and the closest to how you'd retune a live strategy. ## 2. Is it robust, or a lucky spike? - **Parameter sensitivity sweeps**: does the result survive nearby parameter values, or does it live on an isolated spike that a slightly different period would miss? - **Multi asset robustness**: does the same logic work on other markets, or only the one you picked? - **Regime context**: does it work across bull, bear and chop, or only one regime? See the [Robustness reference](/docs/reference/robustness) for what each check means. ## 3. How much was luck? Run **Monte Carlo** to resample the trade order thousands of times. It shows the range of outcomes you could have had and how deep a drawdown to expect. If the 5th percentile path would have wiped you out, the strategy is riskier than the single equity curve suggests. ## 4. Discount for the search If you tried many configurations, read the overfitting aware statistics. A **Deflated Sharpe** that drops far below the raw Sharpe, or an **Overfit Probability (PBO)** at or above 50%, means the result is mostly selection luck. Passing every check makes an edge *plausible*, not certain. Backtests exclude live latency and venue costs, and past performance never guarantees future results. Size accordingly. Background reading: [why overfitting matters](/docs/concepts/overfitting). --- ## Build a portfolio Source: https://docs.texttoquant.com/guides/build-a-portfolio Summary: Assemble several assets into one shared capital book, review the per asset split, and run it. A portfolio runs several assets from one shared pool of capital as a single *book*. This guide takes you from an idea to a scored book. For the full model behind it (contention, rebalancing, risk controls and the report), see the [Portfolios reference](/docs/reference/portfolio). The portfolio builder in the terminal (the steps below) is an Enterprise feature. On Power and Quant you run portfolios through a connected AI agent over [MCP](/docs/api/mcp) instead. In the terminal, switch the sidebar toggle from **Single asset** to **Portfolio**. Or just write a prompt that names two or more assets: the terminal notices and switches for you. Write one prompt that covers every asset, or open the **Visual** tab and add assets by hand. Each asset can carry its own strategy. ```text On the 1d, buy BTCUSDT when the 50 SMA crosses above the 200 SMA and exit when it crosses back below. Do the same on ETHUSDT and on SOLUSDT. ``` Analyze fans your prompt into one parsed strategy per asset. Check each asset's review card: the clause it was assigned ("Split as"), its logic, and its market. You can edit an asset, copy one asset's rules to the whole roster, or remove it. Set the shared **capital** (the whole book trades from one pool), the **contention** rule (who gets the cash when assets signal together), and the backtest **window**. New books start at $30,000 with `rank` contention. Layer on rebalancing ("rebalance monthly to 40% BTC, 30% ETH, 30% SOL"), risk limits ("stop the book at 20%", "at most 3 open positions"), or cross asset gates ("when BTC RSI crosses above 65, if ETH RSI is above 50, buy ETHUSDT"). See the [reference](/docs/reference/portfolio) for every option. Run the book. The report plots the shared equity curve against an equal weight basket, breaks out each asset's contribution, lists a skip ledger of signals that could not be funded, and grades the whole book. Share it as a read only link or export the trades. Running a book bills one backtest credit per asset, so a three asset book costs three credits. Parsing the prompt is free, so review the per asset split before you run. Next: the [Portfolios reference](/docs/reference/portfolio) for contention, rebalancing, risk controls and the full book report. --- ## Create a custom indicator Source: https://docs.texttoquant.com/guides/custom-indicators Summary: Bring your own signal from Pine or a CSV, save it, and reference it in any query with an @ tag. Beyond the [built in library](/docs/reference/indicators), you can author your own indicator (write code, or upload a CSV of values), save it, and then use it inside a strategy exactly like any other series. This guide covers making one; using `@` tags in a query is covered under [entry & exit](/docs/reference/entry-exit#custom-indicators-tags). ## Four ways to bring a series - **JavaScript**: write an indicator in plain JavaScript in the [editor](/editor). It runs in a sandbox against real bars and, unlike Pine, is not pinned to the symbol or timeframe you wrote it on. See [JavaScript indicators](/docs/reference/javascript-indicators) for the full language. - **Python**: the same capability in Python, with the same contract, the same `ta.*` helpers and the same limits. See [Python indicators](/docs/reference/python-indicators). - **Pine** (every plan): write or paste a Pine v6 study in the [editor](/editor) and compile it against real market data. JavaScript, Python and CSV are on Pro and above. - **CSV**: upload a sheet whose columns are your series (e.g. a signal exported from elsewhere), and tag the columns you want to use. Neither is preferred and both produce the same artefact: a saved indicator that recomputes for whatever symbol and timeframe a run uses. Write in whichever you would rather debug. ## Author and save Go to [/editor](/editor). Custom indicators are a plan gated feature. Pick a workspace in the editor: Pine, JavaScript or Python. A study with several plots (Pine `plot()` or a `PLOTS` entry) exposes each plot as its own named column. **Validate** and **lint** check that the script compiles, with no run and no charge. **Preview** compiles and runs it against real data so you can see the plotted output. Give it a name: that name is how you'll reference it in a query. ```javascript // JavaScript export const INPUTS = { length: 14 } export const PLOTS = [{ name: 'signal', kind: 'line' }] export function calc(candles, opts) { const rsi = ta.rsi(candles.close, opts.length) return { signal: rsi.map((v) => (Number.isFinite(v) ? v - 50 : null)) } } ``` ```pine //@version=6 indicator("My Osc", overlay=false) src = close osc = ta.rsi(src, 14) - 50 plot(osc, "signal") ``` ## Reference it in a query Tag a saved indicator with `@` and its name. For an indicator with several plots (or a CSV with several columns), name the specific plot/column. - Single series → `@"My Osc"` - A specific plot or CSV column → tag the column, e.g. `@"My Sheet:signal"` Align the custom series to your strategy's timeframe in the UI, then use it in conditions like any built in indicator. Custom indicators are on Pro and above. ## Everything the phrasing reference describes works on your series too A saved indicator is a series like any other, so the whole [phrasings](/docs/reference/phrasings) vocabulary applies to it: percentile rank, streaks, windows, rolling extremes, correlation, z scores, the lot. Name the plot and say the same thing you would say about RSI. | Say this about your series | What the engine checks | | --- | --- | | `@"My Sheet:signal"` is in the **top 10% of its last 100 readings** | percentile rank of your column | | `@"My Sheet:signal"` has been **above 0 for 5 consecutive bars** | a streak on your column | | `@"My Sheet:signal"` is **2 standard deviations above its 20 bar mean** | a statistical band on your column | | the **daily** `@"My Sheet:signal"` is in its **top decile** (Power) | ranked against *daily* history; reading another timeframe is Power and above | | `@"A:x"` **crosses above** `@"B:y"` | two of your series compared | On a sheet with several plots, name the one you mean: `@"My Sheet:signal"` or `My Sheet.signal`. A named column is matched **exactly**: if the name is wrong the condition reads as no data rather than quietly measuring a different column. Leave the column off and the engine still never guesses: the run uses the sheet's primary series (declared in the indicator, or inferred from the data) and the result tells you which line it used, or, if it cannot tell, the condition reads as no data. It never trades on whichever column happened to come first. ## Marker columns: signals that only print sometimes Many Pine indicators plot a value **only on signal bars** (a buy/sell arrow, a divergence dot) and `na` everywhere else. For those the *presence* of a value is the signal, so say **fires**, **prints** or **appears** rather than comparing it to a number. - `@"WT:Sell"` **fires** → the marker printed on this bar - **no** `@"WT:Sell"` in the last 5 bars → the marker did not print - `@"WT:Sell"` has **fired at least 3 times in the last 20 bars** → counted over a window - **at least 10 bars since** `@"WT:Sell"` **fired** → time since the last one Marker columns rarely hold `1`. The value is usually a price or an oscillator level. Ask for the signal to **fire** and the engine checks whether the bar has a value at all. With strict mode on, a custom query requires the `@` tag, a data source, and a parsed custom condition, so a typo can't silently fall back to a built in indicator. Leave it on when a run must use *your* series and nothing else. Related: [entry & exit, @ tags](/docs/reference/entry-exit#custom-indicators-tags), [indicators](/docs/reference/indicators), [export to TradingView](/docs/guides/export-to-pine). --- ## Screen the market Source: https://docs.texttoquant.com/guides/screener Summary: Scan the whole crypto universe for a setup happening right now: the live counterpart to a backtest. The screener answers a different question from a backtest. Not *"would this have worked?"* but *"where is this setup happening right now?"* It ranks the tradable crypto universe against a condition or a strength score, on fresh data. Find it at [/alpha](/alpha). ## What it does A backtest simulates one strategy over history. The screener does the opposite: it evaluates *many symbols at once, right now*, and hands you a ranked table. It's a discovery tool: use it to find candidates, then take the interesting ones to the terminal to backtest properly. ## Scan types | Scan | What it returns | | --- | --- | | Market scan | The tradable universe with per token strength scores, price, 24h move and volume | | Market regime (RSPS) | A `RUN` / `REVIEW` / `SKIP` verdict per timeframe. RSPS = Relative Strength Pairwise Scores | | Token stats | Forward outcome base rates for a signal condition on a token + timeframe | | Sectors | Crypto sector performance | | Dominance matrix | Pairwise dominance across the top tokens | | Custom benchmark | Correlation and beta of the universe vs a benchmark you choose | | Token multi timeframe | One token's recent OHLCV plus its screener rows across timeframes | | Perfect Token Profiler | A deeper async profile of a single token | ## Run your first scan Go to [/alpha](/alpha). The core scans need no login; alerts and the heavier scans are plan gated. Choose a scan type, a timeframe, and any condition or universe filter. Rows are ranked by strength/score. Each row links out to a chart and to the terminal. Save a filter you'll reuse as a **saved scan**, so you can rerun it in one click later. ## Scan alerts & notifications Turn a scan into a standing alert: get pinged when it starts matching, instead of checking manually. - Create an alert from a scan; manage them in the screener. - Delivery goes to **Telegram or email**. Link a channel under your account first (the API exposes `GET /v1/me/notification-channels` to check which will deliver). - Every plan can hold alerts, including free. What differs is how many may be **active** at once, and the top tiers roll matches into a digest instead of sending one message per hit. See the [pricing page](/pricing). A screener hit is not a backtested edge. It's a live signal. Confirm it by backtesting the setup in the [terminal](/terminal) before you act on it. Over an AI client, the same scans are available as tools on the [MCP server](/docs/api/mcp), and over HTTP under `/v1/screener/*`; see the [endpoint reference](/docs/api/endpoints). Plans and limits are on the [pricing page](/pricing). --- ## Work with the AI copilot Source: https://docs.texttoquant.com/guides/copilot Summary: The in app assistant that helps you compose strategies and interrogate a finished run. The copilot is a chat assistant wired into the product. It doesn't replace the engine. It helps you build a strategy and make sense of a finished run, in plain English. It never scores or tunes the numbers; the engine does that deterministically. The copilot is on Pro and above; a free account has no copilot messages. Robustness checks it points you to are on Power and above. ## Where it appears - **Builder copilot**: alongside the strategy you're composing. Ask it to refine a condition, add a stop, fix an awkward phrasing, or turn a vague idea into a runnable query. - **Results copilot**: a dock in the [terminal](/terminal) after a run. Ask why a metric looks the way it does, what a condition actually did in the trades, or what to check next. ## What you can ask | Ask | It can | | --- | --- | | "tighten the stop", "add a trend filter" | Propose an edit to your current strategy | | "why is the Sharpe low?" | Read the run's metrics and explain them | | "what did the RSI condition actually do?" | Explain a condition against the trade ledger | | "is this overfit?" | Point you at the right robustness check and the [overfit verdict](/docs/reference/robustness) | | "how does this compare to my last run?" | Line up two runs for you | ## What it can't do - It acts on **your current strategy or run**. The results copilot needs a **saved run** to read from, so run the backtest first. - It **never steers the result.** Your query compiles to a fixed spec and the engine scores it; the copilot can suggest a change, but it can't make a losing strategy look like a winner. Same query ⇒ same spec ⇒ same test. The copilot proposes; you decide. Every edit it suggests lands in your editable strategy, so you review and run it. Nothing changes your backtest without you. Related: [write your first strategy](/docs/guides/first-strategy), [analysis & export](/docs/reference/analysis), [robustness](/docs/reference/robustness). --- ## Share, fork & the gallery Source: https://docs.texttoquant.com/guides/share-and-fork Summary: Publish a private, revocable link to a run, fork someone else's strategy, and browse the community gallery. Any finished run can become a link. Sharing is opt in and revocable, forking drops a shared strategy straight into your own terminal, and the gallery is where shared strategies get discovered. ## Share a run A shared backtest opens at a `/s/…` link; a shared portfolio at `/sp/…`. Both are **sanitized, read only reports**: the equity curve, metric tiles and grade, with no internal ids. A viewer sees the report, not your terminal, and one line naming who published it. - **Private by default.** Nothing is shared until you mint a link from the **Share** dialog. The link carries a random 122 bit token, so it can't be guessed or enumerated. - **Revocable.** Hit **Revoke** and the link stops resolving immediately: the page 404s. The same works over the API (`DELETE /v1/backtests/:id/share`) and over MCP (`share_backtest` with `revoke: true`). While a link is live, anyone with the URL can open the report. It just isn't listed or indexed. Revoke it when you're done, and don't share a run you consider sensitive. ## Who a shared run is credited to Every shared report and gallery card names its author. - **Set a username** on [Account → Profile](/account/profile) and that handle is what viewers see, written as `@yourname`. It is 3–20 characters (letters, numbers and underscores) and unique across the platform. - **Without a username, your account email is shown instead.** The Share dialog says which of the two applies before you mint a link, so you can set a handle first. - Social preview images only ever carry a username, never an email, because those images are cached by other services and cannot be recalled. - Changing your handle updates every page that already links to your runs. A released handle is held for 30 days so nobody else can take it and inherit your links, and you can reclaim it at any time. ## Fork a strategy A shared **backtest** page carries a **Fork this strategy** button. It opens *your* terminal with the exact query prefilled: nothing runs automatically, so you review it, tweak it, and run it as your own. It's the fastest way to start from someone else's idea. Shared **portfolios** are view only: there's no fork button on a `/sp/…` page. ## The gallery The [Alpha Gallery](/alpha?tab=gallery) is a ranked board of strategies people have chosen to publish. List one of your own from the **Share** dialog's "list in the gallery" toggle (a run must be shared first). - **Sort** by Sharpe, total return, or Deflated Sharpe (the overfitting adjusted confidence score). - **Filter** by grade, asset, or free text search. - Each card names its author and opens its `/s/…` report, where you can fork it. Related: [read a backtest report](/docs/guides/read-a-report), [MCP server](/docs/api/mcp), [the Alpha Gallery](/alpha?tab=gallery). --- ## Export to TradingView as Pine Script Source: https://docs.texttoquant.com/guides/export-to-pine Summary: Pine Script export: turn a validated strategy into a Pine v6 script. Once a strategy earns its keep, you can take it to TradingView as a Pine script: to chart it, set alerts, or paper trade it on your own broker feed. ## 1. Export the strategy Any parsed strategy or completed run can be exported. Validate it first: export a strategy you actually trust. Use the export action to generate a Pine **v6** script that mirrors the strategy's entries, exits, stops and sizing. Open the Pine editor on TradingView, paste the script, and add it to your chart. Connected an AI agent over the [MCP server](/docs/api/mcp)? Just say “export this to Pine” and it calls `export_pine` (or `POST /v1/export/pine` over REST). Pass a `parsedQuery` or an existing backtest id. Anything Pine can't express faithfully is returned in an `unsupported` list, never silently approximated. ## 2. Expect small differences TradingView and the TextToQuant engine are not the same simulator. Fills, fee models, slippage and intrabar assumptions differ, so the numbers won't match to the decimal. Use the Pine export for charting, alerts and live signals, not to reverify performance. The rigorous, cost aware, look ahead free backtest is the one you ran here. See the [execution model](/docs/concepts/execution-model) for exactly what the engine guarantees. ## 3. Custom indicators If your strategy references a custom indicator, make sure its Pine source is available on the TradingView side too. The export references it by name. Related: [entry & exit logic](/docs/reference/entry-exit), [custom indicators](/docs/reference/entry-exit). --- ## Metrics Source: https://docs.texttoquant.com/reference/metrics Summary: Every performance metric TextToQuant reports, what it means, and how to read it. Every backtest returns the same set of metrics so you can compare strategies on equal footing. This page is the definitive reference: what each number measures, and, just as important, how to read it without fooling yourself. Metrics fall into four groups: **headline** performance, **risk adjusted** return, **trade quality**, and the **overfitting aware** statistics that make a result trustworthy. ## Headline metrics These six appear on every results card. They answer "did it make money, and how?" | Metric | What it measures | How to read it | | --- | --- | --- | | Total return | Net profit or loss over the test window, as a percentage of starting capital | The top line result, but never judge it alone; a big return with a huge drawdown is fragile | | Win rate | Share of trades that closed profitable | High win rate ≠ profitable. A 40% win rate can beat a 70% one if winners are bigger | | Profit factor | Gross profit ÷ gross loss | Above `1.0` is profitable; `1.5`+ is healthy; be suspicious of very high values on few trades | | Sharpe ratio | Return per unit of total volatility | `>1` is good, `>2` is strong, but see the overfitting section before trusting it | | Max drawdown | Largest peak to trough equity drop | The pain metric. Ask yourself: could you sit through this loss live? | | Total trades | Number of closed trades | Sample size. Under ~30 trades, treat every other metric as noisy | A strategy is a trade off between return, risk, and reliability. Total return tells you the reward, max drawdown tells you the risk, and total trades tells you how much to trust either. ### Return over time & exposure Two runs of different lengths, or with very different time in the market, aren't directly comparable on raw return. These normalise for that. | Metric | Meaning | | --- | --- | | Annualized return (CAGR) | Total return expressed as a yearly rate, so a 6 month and a 3 year run compare on equal footing | | Exposure / time in market | Share of bars you actually held a position. A strategy in the market 5% of the time carries very different risk from one always in | | Avg / median bars held | Typical holding time per trade | ## Risk adjusted return Raw return ignores how much risk you took to earn it. These metrics divide reward by risk, so a calm strategy and a wild one become comparable. | Metric | Divides return by | Use it when | | --- | --- | --- | | Sharpe ratio | Total volatility (up and down) | The default risk adjusted score | | Sortino ratio | Downside volatility only | You don't want to be penalised for big *winning* months | | Calmar ratio | Max drawdown | You care most about surviving the worst stretch | Because Sharpe penalises upside and downside equally, a strategy with occasional large gains can look worse than it is. That's when Sortino tells the truer story. Calmar is the one to watch if your real constraint is *"how deep a hole can I tolerate?"* ### More risk adjusted & sizing scores | Metric | Meaning | | --- | --- | | Omega ratio | Probability weighted gains ÷ losses around a threshold, captures return skew the Sharpe misses | | Information ratio | Return *above a passive buy & hold*, divided by the volatility of that excess. Differs from Sharpe, which compares to a flat zero | | SQN (System Quality Number) | Trade expectancy scaled by √trades (Van Tharp), one number for how tradable the system is | | Kelly fraction | The bet size the edge implies. A ceiling to respect, not a target, full Kelly is famously wild | ## Trade quality Headline numbers hide *how* the money was made. These read the trade ledger directly. | Metric | Meaning | | --- | --- | | Expectancy (R) | Average profit per trade, expressed in units of risk (R). Positive expectancy is the whole game | | Average win / loss | Mean profit of winners vs mean loss of losers | | Payoff ratio | Average win ÷ average loss, how much bigger winners are than losers | | R multiple | Each trade's result as a multiple of the risk taken to enter it | | MAE / MFE | Average worst drawdown (adverse) and best unrealised gain (favourable) reached *inside* a trade, how much heat you sat through before the exit | | Success rate | Share of trades that closed on one of your exit rules (stop, target, signal) rather than being force closed at a horizon or the test's end | A profitable strategy needs `win rate × payoff` to clear `1`. A low win rate is fine if the payoff is high (trend following), and a low payoff is fine if the win rate is high (mean reversion). **Win rate** asks whether a trade made money. **Success rate** asks whether it exited the way you designed (on a stop, target or signal) versus being closed because it ran out of time or the test ended. A trade can be a win that never hit your target, or a loss that cleanly hit its stop. Read the two together: a high win rate with a low success rate means your exits aren't doing the work. ## Long vs short The ledger is also split by direction, so you can tell a genuinely two sided edge from one that only works one way. | Metric | Meaning | | --- | --- | | Long / short win rate | Win rate computed separately for long and short trades | | Long / short profit factor, expectancy, avg R | The same trade quality read, per direction | A strategy that's only profitable long in a bull market is a very different bet from one that works both ways, this is where you catch it before it costs you. ## Overfitting aware statistics A raw Sharpe is inflated by how many configurations you tried. Once a parameter search records the trials, these statistics appear on the metric cards, an honest read no other natural language tool ships. | Metric | Corrects for | How to read it | | --- | --- | --- | | Probabilistic Sharpe (PSR) | Short samples & fat tails | Higher = more confident the Sharpe beats 0 | | Deflated Sharpe (DSR) | How many configs you tried | ≥95% = survives the search; a big drop vs PSR = search luck | | Overfit Probability (PBO) | In sample best failing out of sample (grid / joint sweep only) | Low is good; ≥50% = likely overfit | | Haircut Sharpe | Bonferroni correction for T trials | The Sharpe you can still claim after the search | | Min. backtest length | Sample too short for the search | Warns when history can't support that many trials | Your query compiles deterministically to a fixed strategy spec, then a fixed engine scores it. The AI never sees, ranks, or tunes the numbers, so it can't steer toward a good looking result. Same query ⇒ same spec fingerprint ⇒ same test. Signals use only closed bar data (no look ahead). The results header shows a Reproducible chip and, when a hold out split exists, an always on out of sample verdict. ## How grading uses these The letter grade you see on each run is not a single metric. It's a blend across four pillars, so a strategy can't earn an A by maximising return while ignoring risk. | Pillar | Driven mainly by | | --- | --- | | Profit | Total return, profit factor, expectancy | | Risk | Max drawdown, Sharpe / Sortino | | Consistency | Win rate, equity curve smoothness, robustness checks | | Edge | Sample size and the overfitting aware statistics above | Use the grade to triage which runs deserve a closer look, then validate the survivors with out of sample and Monte Carlo before you trust an edge. Ready to see them live? Run a strategy and read its report end to end. --- ## Query syntax Source: https://docs.texttoquant.com/reference/query-syntax Summary: The full grammar of a strategy query. A query is more expressive than "indicator crosses value". This page is the reference for everything the parser understands: the three ways to build a strategy, the expression form for comparisons the flat syntax can't reach, and the capability modules you can stack in a single sentence. ## Build modes You can author the same strategy three ways: they all compile to the same backtest, and you can switch between them mid session. | Mode | Best for | | --- | --- | | AI Assistant (natural language) | Fast drafts from one sentence | | Visual builder | Precise condition trees, no syntax | | AI copilot | Refining either mode by chat | Switch modes at any time. The parsed strategy carries over both ways: a query reads back as a visual tree, and a visual tree reads back as text. ## Expression form For comparisons the flat form can't express, rolling windows, N bars ago, multiples, or any indicator used as an operand, write them inline: | Building block | Example | | --- | --- | | Rolling window | `the 20-bar highest high`, `20-bar mean volume` | | N bars ago | `RSI vs its value 5 bars ago` | | Multiple | `volume above 2× its 20-bar average` | | Aggregation | `highest`, `lowest`, `mean`, `sum`, `std` or `median` over a rolling window | | Value when | `the RSI value the last time price crossed the 200 EMA` (value when) | | Indicator operand | `EMA of RSI`, `MACD histogram vs 0` | The [operators reference](/docs/reference/operators#expression-form) is the canonical list; this table mirrors it. ## Capability modules Eleven building blocks the parser understands, mixable in a single query. | Module | Example phrase | | --- | --- | | Divergence | `buy on bullish divergence on RSI`, `bearish OBV divergence`; see the [supported series](/docs/reference/operators) | | [Performance gates](/docs/reference/entry-exit#performance-gates) | `only trade if performance is above 0%` | | Multi timeframe (Power and above) | `when the daily RSI is above 50` | | Stop plans | `move stop to break-even after 2% profit` | | Time filters | `only on Mondays / NY session` | | Risk guards | `wait at least 5 bars between trades` | | Structure memory | `after price breaks and retests the level` | | Pyramiding | `add on each new 10-day high up to 3 times` | | ATR distance | `then price moves 2 ATR away` | | Cross asset (Power and above) | `when BTC is also above its 200 EMA` | | Volatility regime | `when ATR is in the bottom 10th percentile` | ### Sessions & filters | Filter | Values | | --- | --- | | Sessions (UTC) | New York, London, Tokyo, Sydney | | Calendar | weekdays, hour windows, blackout months | | Risk guards | cooldown bars, max trades/day, consecutive loss circuit break, daily loss stop, drawdown circuit break | | Structure refs | previous bar H/L, own previous, broken level, previous step | Combine modules freely: a daily trend filter, a divergence entry, a break even stop, and Monday only, all in one sentence. ## Parsed attributes Before you run, the parser exposes exactly what it extracted. Every field is editable in the UI. | Field | Notes | | --- | --- | | asset / timeframe | Symbol + bar size | | direction | long or short | | market | spot vs futures | | timeConstraint | relative or absolute dates | | entryConditions | `AND`, `OR`, or [sequential](/docs/reference/operators#combining-conditions) (ordered with `then`) | | exitConditions | OR, stops, targets | | positionSizing | risk %, units, notional | Next: browse the [indicator library](/docs/reference/indicators) or the [operators](/docs/reference/operators) you compare with. --- ## Indicators Source: https://docs.texttoquant.com/reference/indicators Summary: 200+ built in indicators and 61 candlestick patterns. Reference any indicator by name in a query: `RSI(14)`, `the 200 EMA`, `MACD histogram`. The engine computes it for you, aligned to your strategy's timeframe. There are 200+ indicators plus 61 candlestick patterns. ## Indicator families | Family | Members | | --- | --- | | Trend / MA | EMA, SMA, WMA, DEMA, TEMA, TRIMA, KAMA, T3, HMA, McGinley, MAMA/FAMA, ALMA, VWMA, SMMA, ZLEMA (Zero Lag EMA), VIDYA, Linear Regression, TSF | | Channels & stops | Donchian Channel, Moving Average Envelope, Chandelier Exit, Chande Kroll Stop, Williams Alligator; see below | | Momentum | RSI, MACD, Stochastic, Stoch RSI, Fast Stoch, ROC / ROCP / ROCR, Momentum, CCI, CMO, TSI, SMI Ergodic, TRIX, PPO, APO, Ultimate, Awesome, Fisher, Connors RSI, RMI, Coppock, KST, Williams %R, Schaff Trend Cycle, WaveTrend, Relative Vigor Index, Stochastic Momentum Index, Detrended Price Osc, Elder Ray, Mass Index, QStick, Center of Gravity, Chande Forecast, Pretty Good Osc, Random Walk Index, Efficiency Ratio | | Trend strength | ADX, DI+ / DI−, DX, ADXR, Aroon Up/Down, Aroon Osc, Vortex, Supertrend, Parabolic SAR, Choppiness, VHF | | Volatility | ATR, NATR, True Range, Bollinger Bands, %B, Bollinger Bandwidth, Keltner, Std Dev, Variance, Historical Volatility, Ulcer Index, Chaikin Volatility, Relative Volatility Index | | Volume | OBV, VWAP, MFI, CMF, A/D Line, Force Index, Chaikin Osc, Klinger, Ease of Movement, NVI / PVI, PVO, Price Volume Trend, Volume Oscillator, Relative Volume | | Statistics | Z Score, LinReg slope / angle / intercept, Correlation, Beta, Disparity, Balance of Power, Ratio (pair, e.g. ETH/BTC), Percentile Rank | | Cycle (Hilbert) | Dominant Cycle Period, Cycle Phase, Trend Mode, Phasor, SineWave | | Structure / levels | Swing High/Low, Fibonacci, Pivot Points, Volume Profile, Session Open H/L, Prev day / week levels, Ichimoku, Heikin Ashi color | | Smart money / structure | Fair Value Gap, Order Block, Breaker, Liquidity Sweep, BOS, CHoCH, MSS, Structure State, Broken Level | | Derivatives | Open Interest, Funding Rate, Aggregated OI, Aggregated Funding, Liquidations | | Price transforms | Typical, Median, Weighted, Average price, HL2, HLC3, OHLC4 | | Strategy runtime | Rolling Performance, Rolling Drawdown, Rolling Sharpe, Rolling Sortino, Equity, the run's own equity curve as a condition; see [performance gates](/docs/reference/entry-exit#performance-gates) | | Suite composites | AHSM, WT-X, Fourier RSI, TPI, Impulsive Candles, Squeeze Pro, Swing Structure, ADMRS, BB Percent Suite, FSVZO, Rolling VWAP; see below | ## Channels and stops Five classic overlays draw **more than one line**, so name the one you mean. For example "the lower Donchian band", "the alligator's lips", "the chandelier exit": | Indicator | Name it as | Lines you can reference | Defaults | | --- | --- | --- | --- | | Donchian Channel | `donchian_channel` | `upper`, `middle`, `lower` | 20 bars, current bar included | | Moving Average Envelope | `ma_envelope` | `upper`, `middle`, `lower` | SMA 20, ±2.5% | | Chandelier Exit | `chandelier_exit` | `long`, `short` | 22 bars, 3 × ATR | | Chande Kroll Stop | `chande_kroll_stop` | `long`, `short` | 10 bars, 1 × ATR, smoothed 9 | | Williams Alligator | `williams_alligator` | `jaw`, `teeth`, `lips` | 13/8/5, displaced 8/5/3 | Two things worth knowing. A Donchian channel includes the current bar, so "breaks above the 20 bar high" is a break of the *previous* bars' high and compiles to the swing high breakout, while the channel itself is the level a pullback retests. And "a chandelier stop 3 ATR below the highest high" is an ATR trailing stop; the `chandelier_exit` line is for conditions such as "exit when the close crosses below the chandelier exit". ## Suite composites Eleven multi model studies, each ported line by line from its original Pine source. Unlike a plain indicator these compute **several named series at once**, so you pick the one you mean: | Indicator | Name it as | Series you can reference | | --- | --- | --- | | AHSM | `ahsm` | `direction`, `stop`, `hma_exit`, `buy`, `sell`, `tp_long`, `tp_short`, `momentum_long`, `momentum_short` | | WT-X | `wt_x` | `wt`, `signal`, `histogram`, `buy`, `sell`, `bull_div`, `bear_div` (+ regular / hidden pairs) | | Fourier RSI | `frsi` | `frsi`, `signal`, `ma`, `upper`, `lower`, `extreme`, divergence flags | | TPI | `tpi` | `tpi` (−1…1), `alert` | | Impulsive Candles | `impulsive_candles` | `signal` (signed tier −3…3), `tier`, `bull`, `bear`, per tier flags | | Squeeze Pro | `squeeze_pro` | `position`, `squeeze`, `win_rate` | | Swing Structure | `swing_structure` | `trend`, `bos`, `msb`, `swing_high`, `swing_low` | | ADMRS | `admrs` | `score` (five models averaged), `up_trend`, `down_trend`, confirmed pairs | | BB Percent Suite | `bbpct` | `bbpct`, `ob`, `os` | | FSVZO | `fsvzo` | `fsvzo`, `signal`, `histogram`, `ma`, bands, divergence flags | | Rolling VWAP | `rolling_vwap` | `vwap` and five band pairs: `upper1`…`upper3`, `lower1`…`lower3` | Just say which series you mean in plain English: "when AHSM flips long", "buy the squeeze breakout", "when price reclaims the rolling VWAP", "on a break of structure". The parser picks the right one. AHSM and Rolling VWAP are price scale, so you can anchor a stop or a target to them directly: "stop at the AHSM trailing stop", "target the rolling VWAP upper 2 sigma band". The rest are oscillators; use them as a signal exit instead. ## Candlestick patterns (61) | Group | Patterns | | --- | --- | | Doji | Doji, Doji Star, Dragonfly, Gravestone, Long Legged, Rickshaw Man, Takuri | | Single bar | Hammer, Inverted Hammer, Hanging Man, Shooting Star, Belt Hold, Marubozu, Closing Marubozu, Spinning Top, High Wave, Long Line, Short Line | | Two bar | Engulfing, Harami, Harami Cross, Piercing, Dark Cloud Cover, Counterattack, Kicking, Kicking by Length, Matching Low, Homing Pigeon, On Neck, In Neck, Thrusting, Separating Lines, Tasuki Gap | | Three bar+ | Morning / Evening Star, Morning / Evening Doji Star, Three White Soldiers, Three Black Crows, Two Crows, Three Inside, Three Outside, Three Line Strike, Three Stars in South, Identical Three Crows, Abandoned Baby, Advance Block, Stalled, Breakaway, Concealing Baby Swallow, Ladder Bottom, Stick Sandwich, Tristar, Unique Three River, Upside Gap Two Crows, Rising / Falling Three Methods, Mat Hold, Side by Side White Lines, Up / Down Gap Three Methods, Hikkake, Modified Hikkake | Reference funding rate and open interest (per symbol or aggregated) in any query. Leveraged futures backtests also model forced liquidation. ## Custom indicators Your own indicators are first class here too. Author a Pine v6 study or upload a CSV, save it, and reference it in a query with an `@` tag: `@"My Osc"`, or `@"My Sheet:signal"` for a named plot or column. They combine with operators exactly like a built in indicator. See [create a custom indicator](/docs/guides/custom-indicators) and [using @tags](/docs/reference/entry-exit#custom-indicators-tags). Combine indicators with [operators](/docs/reference/operators) to build entry and exit conditions. --- ## JavaScript indicators Source: https://docs.texttoquant.com/reference/javascript-indicators Summary: Write an indicator in plain JavaScript: the three exports, ta.* helpers, sandbox limits, and the NaN warm up rule. You can write an indicator in JavaScript instead of Pine. It runs in an isolated sandbox on our servers, computes against the same bars a backtest uses, and, unlike a Pine indicator, is **not pinned to the symbol or timeframe you wrote it on**. The same saved indicator recomputes for whatever a run happens to use. The same capability exists in [Python](/docs/reference/python-indicators), with the same contract, the same `ta.*` helpers computing the same numbers, and the same limits. Pick whichever you would rather write in. Everything on this page is enforced by the compiler, so when you break a rule you get a message with a line number rather than a wrong number. ## The shape of an indicator Exactly three exports. Nothing else is required, and nothing else is special. ```javascript export const INPUTS = { length: 14, overbought: 70, } export const PLOTS = [ { name: 'rsi', kind: 'line' }, { name: 'hot', kind: 'signal' }, ] export function calc(candles, opts) { const rsi = ta.rsi(candles.close, opts.length) return { rsi, hot: rsi.map((v) => (Number.isFinite(v) && v > opts.overbought ? 1 : 0)), } } ``` - **`INPUTS`**: editable defaults. Numbers and `true`/`false` only; each becomes a control in the settings panel. Up to 24. An input can also [describe itself](#describing-an-input), its range, step, label and section. - **`PLOTS`**: what you draw. Each `name` becomes a chart series *and* something you can reference in a query, so names must be unique. Up to 12. - **`calc(candles, opts)`**: returns one array per declared plot, each exactly as long as the bars. `opts` is your `INPUTS` with any user overrides already applied. ## Describing an input An input can be a bare default, or an object that says more about it: ```javascript export const INPUTS = { length: 14, // just a default mult: { default: 2, min: 0.1, max: 10, step: 0.1, // …or a description label: 'ATR Multiplier', tooltip: 'Stop distance.' }, } ``` Both forms mean the same thing to `calc`, `opts.mult` is the number `2` either way. What changes is the control the settings panel builds. **Why bother.** Without a description the panel has only the default to go on, and a default cannot tell it everything. `length: 14` and `mult: 2` are both whole numbers, but one is a bar count and the other is a band width, and a band width you cannot set to 2.5 is broken. Writing `2.0` does not help: Python and JavaScript both hand the panel a plain `2`. | Field | What it does | | --- | --- | | `default` | Required. The value the indicator runs at until someone changes it. | | `integer` | Whole numbers only. Without it, a number input accepts fractions. | | `min` / `max` | The range the control clamps to. | | `step` | How much the +/− buttons move. Defaults to `1` for a whole number default, else `0.1`. | | `label` | What the user reads instead of the variable name. | | `tooltip` | One or two sentences, shown on the ⓘ beside the control. | | `group` | Section heading. Inputs sharing a group render under one header. | | `inline` | Inputs sharing an inline key pack onto one row. | | `options` / `optionLabels` | A dropdown. Values stay numeric; the labels are what the user reads. | Grouping and dropdowns: ```javascript export const INPUTS = { fast: { default: 12, integer: true, group: 'Lengths', inline: 'ma' }, slow: { default: 26, integer: true, group: 'Lengths', inline: 'ma' }, mode: { default: 0, options: [0, 1, 2], optionLabels: ['EMA', 'SMA', 'WMA'], group: 'Method' }, } ``` `fast` and `slow` sit on one row under a **LENGTHS** heading; `mode` is a dropdown showing the three names, and `opts.mode` is `0`, `1` or `2`. **What is checked when you save.** A default outside its own `min`/`max`, a `min` above its `max`, a `step` of zero, a dropdown default that is not one of the options, an `integer` input with a fractional bound, a range on a `true`/`false` input, or a misspelled field, each is refused with a line number rather than silently ignored. **Overrides are held to what you declared.** A value outside the range is clamped to it; one that is not a listed option falls back to the default. That applies in the chart and in a backtest alike, so the two cannot run your indicator at different values. An input with no description behaves exactly as it always has: a number field with no bounds, a step matched to the default's shape, and the variable name title cased as its label. ## `candles` is columnar, not a list of bars You get arrays, one entry per bar, **oldest first**: ```javascript candles.time // epoch milliseconds candles.open candles.high candles.low candles.close candles.volume candles.hl2 // (high + low) / 2 candles.hlc3 // (high + low + close) / 3 candles.ohlc4 // (open + high + low + close) / 4 candles.bar_index // 0, 1, 2, … candles.length // number of bars ``` This is the same shape the `ta.*` helpers take and return, so `candles.close` goes straight into `ta.ema(...)` with nothing to convert at either end. ## Warm up is `NaN`, not `null` This is the one rule that catches everyone, and it fails **silently**, you get a confident looking number instead of an error. A helper has no value until it has enough bars. `ta.ema(close, 26)` has nothing to say for its first 25 bars, and it marks those bars **`NaN`**. `v !== null` is **true** for `NaN`, so it lets warm up straight through. Test a helper's output with **`Number.isFinite(v)`**. ```javascript // WRONG: reports a confident downtrend for the first 26 bars, before either EMA exists. trend: fast.map((f, i) => (f !== null && slow[i] !== null ? (f > slow[i] ? 1 : -1) : null)) // RIGHT trend: fast.map((f, i) => { const s = slow[i] if (!Number.isFinite(f) || !Number.isFinite(s)) return null return f > s ? 1 : -1 }) ``` The rule reverses on the way out: what you **return** may use `null` for "no value on this bar", and any `NaN` or `Infinity` you return is converted to `null` for you, so a stray divide by zero cannot reach the chart as a broken number. ## Plot kinds | kind | use it for | drawn as | | --- | --- | --- | | `line` | a continuous value | a line on the chart | | `signal` | "this fired on this bar", 0 or 1 | a marker | | `state` | a small integer regime, e.g. `-1` / `0` / `+1` | a stepped series | | `color` | one **colour** per bar, painting another layer | nothing on its own; see below | ## Drawing Your indicator does not have to be grey lines. Everything Pine can draw, you can declare, and each export below is the equivalent of the Pine call named beside it. ```javascript export const META = { overlay: true, precision: 2 } // indicator(overlay=…) export const PLOTS = [ { name: 'hist', style: 'histogram', colors: 'histColor' }, // plot(style=, color=) { name: 'histColor', kind: 'color' }, // the per-bar colour channel ] export const LEVELS = [{ value: 70, color: 'red', linestyle: 'dashed', label: 'OB' }] // hline() export const FILLS = [{ from: 'upper', to: 'lower', color: 'blue', transp: 92 }] // fill() export const SHAPES = [{ plot: 'buy', shape: 'triangleup', location: 'belowbar', text: 'BUY' }] export const ARROWS = [{ plot: 'netFlow', colorUp: 'green', colorDown: 'red' }] // plotarrow() export const BGCOLOR = 'regimeColor' // bgcolor() export const BARCOLOR = 'trendColor' // barcolor() export const CANDLES = [{ open: 'o', high: 'h', low: 'l', close: 'c' }] // plotcandle() ``` **`META.overlay` is the field worth setting first.** It is the difference between a moving average drawn *on* the price and one drawn in an empty pane underneath it, and it decides whether the preview chart puts your indicator on the price's scale. A plot may declare `style` (`line`, `stepline`, `histogram`, `columns`, `area`, `circles`, `cross`), `color`, `transp`, `colors`, `linewidth`, `linestyle`, `overlay`, `display`, `offset`, `precision`, `histbase` and `joinNulls`. ### Arrows say *how much* `SHAPES` marks **when** something happened; every marker is the same size. `ARROWS` says **how much**: the named plot's sign picks the direction, and its magnitude scales the arrow's length against the largest absolute value in the series. ```javascript export const PLOTS = [{ name: 'netFlow', kind: 'line' }] export const ARROWS = [{ plot: 'netFlow', colorUp: 'green', colorDown: 'red', minHeight: 4, maxHeight: 40 }] ``` It needs a `line` plot, a 0/1 `signal` has no magnitude to scale by, and that is refused rather than drawn at one uniform height. ### Per bar colour A `kind: 'color'` plot returns one colour **string** per bar: `'#26a69a'`, `'#26a69a80'`, `'rgb(38,166,154)'`, or a name like `'red'`, or `null` for "leave this bar alone". It never becomes a data column and can never be used in a strategy condition, because "above 50" means nothing for `#26a69a`. It exists only to paint whatever names it: ```javascript export const PLOTS = [ { name: 'rsi', kind: 'line' }, { name: 'zone', kind: 'color' }, ] export const BGCOLOR = 'zone' export function calc(candles, opts) { const rsi = ta.rsi(candles.close, opts.length) return { rsi, zone: rsi.map(v => !Number.isFinite(v) ? null : v > 70 ? '#ef535020' : v < 30 ? '#26a69a20' : null), } } ``` A colour channel that nothing points at is refused at compile time, it would cost a full series per bar and draw nothing. ### The editor shows you the price Compile draws your indicator against the real candles it was computed from. An `overlay` study shares the price's scale, so you can see whether your band actually tracks the price; an oscillator keeps its own range with the price shown separately for context, so a 0-100 series is not flattened against the asset's price. ## There are no packages An indicator is a **single self contained file**. There is no npm, and no way to reach the outside world: ```javascript import _ from 'lodash' // ✗ Cannot import "lodash" const fs = require('fs') // ✗ require is not defined await fetch('https://…') // ✗ fetch is not defined process.env.SECRET // ✗ process is not defined Math.random() // ✗ throws Date.now() // pinned to the last bar, not the wall clock ``` That is not a limitation we plan to lift, because it is what makes a backtest **reproducible**: a run today and the same run in a year must produce identical numbers. A floating dependency or a network call would break that, and a package you did not audit would be running against your account. **What to do instead:** - **Use `ta.*`**: 65 helpers cover most of what a package would give you. - **Paste the function in.** You have 128KB. A pure JavaScript helper works exactly as it would anywhere else: ```javascript function median(values) { const sorted = [...values].sort((a, b) => a - b) return sorted[sorted.length >> 1] } ``` - **Import your own indicators**: see below. ## What you *can* import: your own indicators An indicator may import another one **already saved on your account**, by name with an `@` prefix. This is how you build on a baseline, and how you keep shared maths in one place instead of pasting it into every file. ```javascript // Reuse another indicator's OUTPUT import { calc as base, INPUTS as baseInputs } from '@my_baseline' // …or just a helper it exports import { median } from '@my_utils' export const INPUTS = { length: 20 } export const PLOTS = [{ name: 'smoothed', kind: 'line' }] export function calc(candles, opts) { const b = base(candles, { ...baseInputs, length: opts.length }) return { smoothed: ta.sma(b.rsi, 5) } } ``` Imports are resolved **before** your code runs, so a problem is reported up front with the name or the chain that caused it: - a name you have not saved → *You have no JavaScript indicator named "x".* - two indicators importing each other → *Circular import: a → b → c → a.* - more than 5 levels deep, or more than 16 indicators Deleting an indicator that others import is refused, and the response names the ones that depend on it, so you can edit those first. ## Limits | | | | --- | --- | | Run time | 3 seconds | | Memory | 64 MB | | Code size | 128 KB | | Plots | 12 | | Inputs | 24 | | Imported indicators | 16, up to 5 levels deep | A timeout or a memory cap is reported as its own kind of failure: your code is valid, it just could not finish inside the budget. ## The loop: validate, preview, save Checks syntax, the module shape and that every `@import` resolves. Loads **no market data** and never calls `calc`, so it answers in milliseconds. A green result means "this is a valid indicator", not "this works". Runs `calc` against real bars for the symbol, timeframe and range in the toolbar, and plots the result. This is the step that finds a runtime error, a timeout, or a series of the wrong length. Compiles first and saves only if it compiles, a saved indicator that does not run is a landmine that would fail inside a backtest, far from the editor where you could fix it. Drafts autosave as you type, and **History** in the editor toolbar restores earlier ones. ## Using it in a query Exactly like any other custom indicator, reference the saved name with an `@` tag: ```text Buy BTC when @my_osc crosses above 0, 4H last 180 days ``` A multi plot indicator exposes each plot name as its own series. See [Create a custom indicator](/docs/guides/custom-indicators) for the full query side. ## Errors you will meet | message | what it means | | --- | --- | | *Your indicator does not export `PLOTS`* | Declare what you draw, even if it is one line. | | *`x` is null on every bar* | Usually a warm up guard that is never satisfied; check for `!== null` where you meant `Number.isFinite`. | | *`x` is declared in PLOTS, so calc must return it* | Every declared plot needs an array back, null filled during warm up rather than omitted. | | *Every series must line up 1:1 with the bars* | Fill warm up bars instead of skipping them. | | *Cannot import "lodash"* | There are no packages; see above. | | *Your indicator went too deep* | Almost always a function calling itself with no stopping condition. | --- ## Python indicators Source: https://docs.texttoquant.com/reference/python-indicators Summary: Write an indicator in Python: the three names, ta.* helpers, sandbox limits, and the NaN warm up rule that `is not None` will not catch. You can write an indicator in Python instead of Pine or JavaScript. It runs in an isolated sandbox on our servers, computes against the same bars a backtest uses, and, unlike a Pine indicator, is **not pinned to the symbol or timeframe you wrote it on**. The same saved indicator recomputes for whatever a run happens to use. It is the same capability as [JavaScript indicators](/docs/reference/javascript-indicators), in a different language: the same contract, the same `ta.*` helpers computing the same numbers, the same limits. Pick whichever you would rather write in. Everything on this page is enforced by the compiler, so when you break a rule you get a message with a line number rather than a wrong number. ## The shape of an indicator Exactly three module level names. Nothing else is required, and nothing else is special. ```python INPUTS = { "length": 14, "overbought": 70, } PLOTS = [ {"name": "rsi", "kind": "line"}, {"name": "hot", "kind": "signal"}, ] def calc(candles, opts): rsi = ta.rsi(candles["close"], opts["length"]) return { "rsi": rsi, "hot": [0 if ta.na(v) else (1 if v > opts["overbought"] else 0) for v in rsi], } ``` - **`INPUTS`**: editable defaults. Numbers and `True`/`False` only; each becomes a control in the settings panel. Up to 24. An input can also [describe itself](#describing-an-input), its range, step, label and section. - **`PLOTS`**: what you draw. Each `name` becomes a chart series *and* something you can reference in a query. - **`calc`**: required. Returns one list per plot, each exactly as long as the bars. ## Describing an input An input can be a bare default, or a dict that says more about it: ```python INPUTS = { "length": 14, # just a default "mult": {"default": 2.0, "min": 0.1, "max": 10.0, "step": 0.1, # ...or a description "label": "ATR Multiplier", "tooltip": "Stop distance."}, } ``` Both forms mean the same thing to `calc`, `opts["mult"]` is the number `2.0` either way. What changes is the control the settings panel builds. **Why bother, in Python especially.** Writing `2.0` instead of `2` looks like it should be enough, and it is not: the contract reaches the panel as JSON, where `2.0` and `2` are the same number. Without a description the panel has only that number to go on, and `"length": 14` and `"mult": 2.0` look identical to it: one is a bar count, the other a band width, and a band width you cannot set to 2.5 is broken. | Field | What it does | | --- | --- | | `default` | Required. The value the indicator runs at until someone changes it. | | `integer` | Whole numbers only. Without it, a number input accepts fractions. | | `min` / `max` | The range the control clamps to. | | `step` | How much the +/− buttons move. Defaults to `1` for a whole number default, else `0.1`. | | `label` | What the user reads instead of the variable name. | | `tooltip` | One or two sentences, shown on the ⓘ beside the control. | | `group` | Section heading. Inputs sharing a group render under one header. | | `inline` | Inputs sharing an inline key pack onto one row. | | `options` / `optionLabels` | A dropdown. Values stay numeric; the labels are what the user reads. | Grouping and dropdowns: ```python INPUTS = { "fast": {"default": 12, "integer": True, "group": "Lengths", "inline": "ma"}, "slow": {"default": 26, "integer": True, "group": "Lengths", "inline": "ma"}, "mode": {"default": 0, "options": [0, 1, 2], "optionLabels": ["EMA", "SMA", "WMA"], "group": "Method"}, } ``` `fast` and `slow` sit on one row under a **LENGTHS** heading; `mode` is a dropdown showing the three names, and `opts["mode"]` is `0`, `1` or `2`. **What is checked when you save.** A default outside its own `min`/`max`, a `min` above its `max`, a `step` of zero, a dropdown default that is not one of the options, an `integer` input with a fractional bound, a range on a `True`/`False` input, or a misspelled key, each is refused with a line number rather than silently ignored. **Overrides are held to what you declared.** A value outside the range is clamped to it; one that is not a listed option falls back to the default. That applies in the chart and in a backtest alike, so the two cannot run your indicator at different values. An input with no description behaves exactly as it always has. ## This is MicroPython, not CPython Indicators run on **MicroPython**, a compact Python. Almost anything you would write inside an indicator works unchanged: loops, comprehensions, functions, classes, `math`, string formatting without `f"..."` strings, but the standard library is smaller and there is no `pip`. What that rules out, and what to use instead: | you might reach for | use instead | | --- | --- | | `pandas` | `ta.*`, it already speaks whole series | | `numpy` | `ulab.numpy`, which ships in the sandbox: `from ulab import numpy as np` | | `scipy` | `ta.*`, or write the maths out, you have 128 KB | | `datetime` | `candles["time"]` is epoch milliseconds | | `re` | plain string methods: `startswith`, `split`, `in` | ### What you can import The full list of importable standard library modules: | module | what you get | | --- | --- | | `math`, `cmath` | the usual maths; `cmath` for complex numbers | | `ulab.numpy` | array maths, the `numpy` stand in: `from ulab import numpy as np` | | `itertools` | `accumulate`, `chain`, `islice`, `count`, `cycle`, … | | `functools` | `reduce`, `partial` | | `heapq` | `heappush` / `heappop` / `heapify`, rolling min/max in O(log n) | | `operator` | `attrgetter`, `add`, `lt`, … (this build has no `itemgetter`, use a lambda) | | `collections` | `deque`, `namedtuple`, `OrderedDict` | | `array` | compact typed arrays when a list of floats is too heavy | | `json` | `dumps` / `loads`, if you keep configuration in a string | Anything not on that list is refused at import, by name, before your code runs. (One footnote: `ucollections`, the MicroPython native module `collections` wraps, is importable too, but `collections` is the spelling to use.) ## `candles` is columnar, not a list of bars You get lists, one entry per bar, **oldest first**: ```python candles["time"] # epoch milliseconds candles["open"] candles["high"] candles["low"] candles["close"] candles["volume"] candles["hl2"] # (high + low) / 2 candles["hlc3"] # (high + low + close) / 3 candles["ohlc4"] # (open + high + low + close) / 4 candles["bar_index"] # 0, 1, 2, … candles["length"] # number of bars ``` This is the same shape the `ta.*` helpers take and return, so `candles["close"]` goes straight into `ta.ema(...)` with nothing to convert at either end. ## Warm up is `NaN`, and `is not None` will not catch it This is the one rule that catches everyone, and it fails **silently**, you get a confident looking number instead of an error. A helper has no value until it has enough bars. `ta.ema(close, 26)` has nothing to say for its first 25 bars, and it marks those bars **`NaN`**. `v is not None` is **True** for `NaN`, so the Pythonic looking guard lets warm up straight through. Test a helper's output with **`ta.na(v)`**. ```python # WRONG: reports a confident downtrend for the first 26 bars, before either EMA exists. trend = [1 if f > s else -1 for f, s in zip(fast, slow)] # RIGHT trend = [] for i in range(len(fast)): f, s = fast[i], slow[i] if ta.na(f) or ta.na(s): trend.append(None) else: trend.append(1 if f > s else -1) ``` `ta.na(v)` is `True` for `NaN`, for infinity **and** for `None`, so it is one guard for every "no value" the engine can hand you, which is why it is preferable to `math.isfinite`. The rule reverses on the way out: what you **return** may use `None` for "no value on this bar", and any `NaN` or infinity you return is converted to `null` for you, so a stray divide by zero cannot reach the chart as a broken number. ## Plot kinds | kind | use it for | drawn as | | --- | --- | --- | | `line` | a continuous value | a line on the chart | | `signal` | "this fired on this bar", 0 or 1 | a marker | | `state` | a small integer regime, e.g. `-1` / `0` / `+1` | a stepped series | | `color` | one **colour** per bar, painting another layer | nothing on its own; see below | ## Drawing Your indicator does not have to be grey lines. Everything Pine can draw, you can declare, and each name below is the equivalent of the Pine call beside it. ```python META = {"overlay": True, "precision": 2} # indicator(overlay=…) PLOTS = [ {"name": "hist", "style": "histogram", "colors": "histColor"}, # plot(style=, color=) {"name": "histColor", "kind": "color"}, # the per-bar colour channel ] LEVELS = [{"value": 70, "color": "red", "linestyle": "dashed", "label": "OB"}] # hline() FILLS = [{"from": "upper", "to": "lower", "color": "blue", "transp": 92}] # fill() SHAPES = [{"plot": "buy", "shape": "triangleup", "location": "belowbar", "text": "BUY"}] ARROWS = [{"plot": "netFlow", "colorUp": "green", "colorDown": "red"}] # plotarrow() BGCOLOR = "regimeColor" # bgcolor() BARCOLOR = "trendColor" # barcolor() CANDLES = [{"open": "o", "high": "h", "low": "l", "close": "c"}] # plotcandle() ``` **`META["overlay"]` is the field worth setting first.** It is the difference between a moving average drawn *on* the price and one drawn in an empty pane underneath it, and it decides whether the preview chart puts your indicator on the price's scale. A plot may declare `style` (`line`, `stepline`, `histogram`, `columns`, `area`, `circles`, `cross`), `color`, `transp`, `colors`, `linewidth`, `linestyle`, `overlay`, `display`, `offset`, `precision`, `histbase` and `joinNulls`. ### Arrows say *how much* `SHAPES` marks **when** something happened; every marker is the same size. `ARROWS` says **how much**: the named plot's sign picks the direction, and its magnitude scales the arrow's length against the largest absolute value in the series. ```python PLOTS = [{"name": "netFlow", "kind": "line"}] ARROWS = [{"plot": "netFlow", "colorUp": "green", "colorDown": "red", "minHeight": 4, "maxHeight": 40}] ``` It needs a `line` plot, a 0/1 `signal` has no magnitude to scale by, and that is refused rather than drawn at one uniform height. ### Per bar colour A `"kind": "color"` plot returns one colour **string** per bar: `"#26a69a"`, `"#26a69a80"`, `"rgb(38,166,154)"`, or a name like `"red"`, or `None` for "leave this bar alone". It never becomes a data column and can never be used in a strategy condition, because "above 50" means nothing for `#26a69a`. It exists only to paint whatever names it: ```python PLOTS = [ {"name": "rsi", "kind": "line"}, {"name": "zone", "kind": "color"}, ] BGCOLOR = "zone" def calc(candles, opts): rsi = ta.rsi(candles["close"], opts["length"]) zone = [] for v in rsi: if v != v: # NaN, still warming up zone.append(None) elif v > 70: zone.append("#ef535020") elif v < 30: zone.append("#26a69a20") else: zone.append(None) return {"rsi": rsi, "zone": zone} ``` A colour channel that nothing points at is refused at compile time, it would cost a full series per bar and draw nothing. ### The editor shows you the price Compile draws your indicator against the real candles it was computed from. An `overlay` study shares the price's scale, so you can see whether your band actually tracks the price; an oscillator keeps its own range with the price shown separately for context, so a 0-100 series is not flattened against the asset's price. ## There are no packages An indicator is a **single self contained file**. There is no `pip`, and no way to reach the outside world: ```python import pandas as pd # ✗ module 'pandas' is not available in a Python indicator import requests # ✗ module 'requests' is not available import os # ✗ module 'os' is not available import sys # ✗ module 'sys' is not available open("/etc/passwd") # ✗ open is not available import random # ✗ module 'random' is not available import time # ✗ module 'time' is not available import datetime # ✗ module 'datetime' is not available ``` `random`, `time` and `datetime` are absent for a different reason from the rest: not safety, but **reproducibility**. A backtest run today and the same run in a year must produce identical numbers, and a clock or a random draw would break that. There is no wall clock inside a historical bar, the only time that exists is `candles["time"]`, the bar's own epoch milliseconds. **What to do instead:** - **Use `ta.*`**: 65 helpers, numerically identical to the JavaScript engine's. - **Use `ulab.numpy`** for array maths: `from ulab import numpy as np`. - **Use the standard library that is there**: `itertools`, `functools`, `heapq`, `operator`, `collections`, `array`, `json`, `math`, `cmath`. See the table above. - **Write the function out.** You have 128 KB, and plain Python works exactly as it would anywhere else: ```python def median(values): s = sorted(values) return s[len(s) // 2] ``` - **Import your own indicators**: see below. ## What you *can* import: your own indicators An indicator may import another one **already saved on your account**, through the reserved `ttq` package. This is how you build on a baseline, and how you keep shared maths in one place instead of pasting it into every file. ```python # The whole indicator, as a module from ttq import my_baseline # …or just what you need from ttq.my_utils import median INPUTS = {"length": 20} PLOTS = [{"name": "smoothed", "kind": "line"}] def calc(candles, opts): b = my_baseline.calc(candles, {"length": opts["length"]}) return {"smoothed": ta.sma(b["rsi"], 5)} ``` `import ttq.my_baseline` works too. `ttq` is the Python spelling of the JavaScript engine's `@name` a Python import names an identifier rather than a string, so `@` is a syntax error there rather than a convention. Imports are resolved **before** your code runs, so a problem is reported up front with the name or the chain that caused it: - a name you have not saved → *You have no Python indicator named "x".* - two indicators importing each other → *Circular import: a → b → c → a.* - more than 5 levels deep, or more than 16 indicators Deleting an indicator that others import is refused, and the response names the ones that depend on it, so you can edit those first. ## Limits | | | | --- | --- | | Run time | 3 seconds | | Memory | 64 MB | | Code size | 128 KB | | Plots | 12 | | Inputs | 24 | | Imported indicators | 16, up to 5 levels deep | Identical to the JavaScript engine's, because they are the same limits enforced in the same place. A timeout or a memory cap is reported as its own kind of failure: your code is valid, it just could not finish inside the budget. ## The loop: validate, preview, save Checks syntax, the module shape and that every `ttq` import resolves. Loads **no market data** and never calls `calc`, so it answers in milliseconds. A green result means "this is a valid indicator", not "this works". Runs `calc` against real bars for the symbol, timeframe and range in the toolbar, and plots the result. This is the step that finds a runtime error, a timeout, or a series of the wrong length. Compiles first and saves only if it compiles, a saved indicator that does not run is a landmine that would fail inside a backtest, far from the editor where you could fix it. Drafts autosave as you type, and **History** in the editor toolbar restores earlier ones. The JavaScript editor has a full language service, so a typo gets a red squiggle as you type. The Python editor has syntax highlighting and completion but no live type checking, so **Validate** is where a typo surfaces. It is instant and loads no data, run it often. ## Using it in a query Exactly like any other custom indicator, reference the saved name with an `@` tag: ```text Buy BTC when @my_osc crosses above 0, 4H last 180 days ``` The `@` here is the QUERY syntax and is the same for every custom indicator, whatever language it was written in. It is unrelated to imports, which inside a Python file use `ttq`. A multi plot indicator exposes each plot name as its own series. See [Create a custom indicator](/docs/guides/custom-indicators) for the full query side. ## Errors you will meet | message | what it means | | --- | --- | | *Your indicator does not export `PLOTS`* | Declare what you draw, even if it is one line. | | *`x` is null on every bar* | Usually a warm up guard that is never satisfied; check for `is not None` where you meant `ta.na`. | | *`x` is declared in PLOTS, so calc must return it* | Every declared plot needs a list back, `None` filled during warm up rather than omitted. | | *Every series must line up 1:1 with the bars* | Fill warm up bars instead of skipping them. | | *module 'pandas' is not available in a Python indicator* | There are no packages; see above. | | *IndentationError* | Mixed indent widths. The editor uses 4 spaces; MicroPython will not guess. | | *Your indicator called itself too many times* | Almost always a function calling itself with no stopping condition. | --- ## Markets & data Source: https://docs.texttoquant.com/reference/markets Summary: The asset classes you can backtest, covering crypto, stocks, forex and metals, and how a symbol resolves to a data venue. TextToQuant is not crypto only. The same engine backtests crypto, equities, forex and precious metals: crypto on every plan, and stocks, forex and metals from Power. You name the symbol in your query and the engine classifies it and routes it to the right data venue automatically. ## Asset classes | Class | How to name it | Notes | | --- | --- | --- | | Crypto (spot) | `BTC`, `ETH`, `SOL`, or a full pair `BTCUSDT` / `BTC/USDT` | A bare base resolves to its USDT pair on the first venue that lists it (Binance → Bybit → KuCoin → Bitget). Tokens with no major exchange listing still resolve to TradingView's `CRYPTO:` aggregate index | | Crypto (futures / perps) | the same symbol, run on the futures market | Funding, open interest and forced liquidation modelling live here only; see below | | Stocks | a ticker like `AAPL`, `TSLA`, `SPY`, or exchange qualified `NASDAQ:AAPL` | 1-5 letter tickers, and dotted share classes like `BRK.B`, route to the equities data path | | Forex | a 6 letter pair like `EURUSD`, `GBPJPY`, or `EUR/USD` | Both halves must be real currencies, that's what separates FX `EURUSD` from the crypto stablecoin pair `EURUSDT` | | Metals | `XAU`, `XAG`, `XPT`, `XPD`, or plain `gold` / `silver` / `platinum` / `palladium` | Gold, silver, platinum and palladium against USD | ## Naming a symbol in a query Write the symbol straight into the sentence. The parser and engine work out the asset class for you, so a stock strategy reads exactly like a crypto one. The two examples below are a stock and a metal, so they run on Power and above. To force a specific venue or resolve an ambiguous ticker, qualify it with a prefix: `NASDAQ:AAPL`, `BINANCE:BTCUSDT`, `FX:EURUSD`, `OANDA:XAUUSD`. Crypto runs on the spot market by default; ask for `futures` or a `perp` to run the derivatives book (and if a spot pair isn't listed, the engine falls back to futures automatically). The market you land on shows up in the parsed attributes before you run. ## What's crypto only Funding rate, open interest, aggregated OI/funding and the forced liquidation model apply to crypto (and crypto futures) only. A stock, forex or metals backtest has none of them, referencing them in such a query won't produce a signal. ## Data & history - **Crypto candles** come from the major centralized venues (Binance, Bybit, KuCoin, Bitget); a coin registry lets even unlisted or DEX only tokens classify as crypto. - **Stocks, forex and metals** are served through the TradingView data oracle, on the same path (with session gap tolerance and calendar aware warm up). - **History depth** is the same on every plan: it is how far the data reaches. Crypto reaches the pair's listing; stocks, forex and metals reach a fixed number of bars back, so the window shrinks with the timeframe (see [Limits](/docs/reference/limits)). What your [plan](/docs/concepts/plans-and-credits) changes is how many years a run may span (Free runs the last three) and which timeframes you may use. Monthly bars use true calendar closes, and indicators are seeded with warm up bars before your start date so a 200 period average is already correct on bar 1 (see the [execution model](/docs/concepts/execution-model)). Related: [query shape](/docs/start/query-shape), [indicators](/docs/reference/indicators), [plans & credits](/docs/concepts/plans-and-credits). --- ## Limits Source: https://docs.texttoquant.com/reference/limits Summary: What each asset class and each indicator language can and cannot do, the bar ceilings on a run, and which timeframes your plan may use. Everything here is enforced by the engine, so you meet these as a refusal before a run starts, not as a wrong number afterwards. Three kinds of limit get mixed up constantly, and they behave differently: | Kind | Example | What changes it | | --- | --- | --- | | **Capability** | The engine runs 16 timeframes | Nothing you can buy. It is what the code does. | | **Entitlement** | Free and Pro may use 4 of them, Power 8, Quant and Enterprise all 16 | Your [plan](/pricing). | | **Data** | No stock has premarket bars | Nothing. The data does not exist. | ## Timeframes The engine runs sixteen, and so does the parser: ```text 1m 3m 5m 15m 30m 45m 1h 2h 4h 6h 8h 12h 1d 3d 1w 1M ``` `1M` is a **month**. `1m` is one minute. Case is the only thing that separates them. Which of the sixteen you may run is a plan entitlement, and the pickers in the app show exactly your list, so you cannot choose one you do not have. The [pricing page](/pricing) has the per plan count. Over the API or MCP, `get_usage` returns `allowedTimeframes`. Asking for one outside your list parses cleanly and is then refused with `plan_timeframe_limit`. `2d`, `2w`, `90m` and `3h` have no bar duration in the engine, on any plan. They are refused at parse with the runnable list attached, not silently rounded to something near them. ## How much history one run may ask for Years and bars are separate limits and you meet whichever you hit first. "Unlimited history" on a plan means unlimited **years**. The **bar** ceilings are the same for everyone: | Run | Ceiling | | --- | --- | | Any single run | 2,000,000 bars | | One asset, built in indicators only | 4,000,000 bars | | A portfolio, summed across every sleeve | 500,000 bars | | Any run using a custom indicator | 350,000 bars | A run over the ceiling is refused up front, naming a shorter range or a higher timeframe, rather than being accepted and dying partway through. Ten years of 1 minute data is about 5.2 million bars, so it is over the limit on every plan; the same decade on 5m is not. ## Asset classes | | Crypto | Equities | Forex | Metals | | --- | --- | --- | --- | --- | | **Name it** | `BTC`, `BTCUSDT` | `AAPL`, `NASDAQ:AAPL` | `EURUSD` | `XAUUSD`, `gold` | | **Data** | Binance and other major venues | TradingView oracle | TradingView oracle | TradingView oracle | | **Session** | 24/7 | US regular hours, 09:30 to 16:00 ET | Continuous 24x5 | Continuous 24x5 | | **Trading year** | 365 days | 252 days | 260 days | 260 days | | **Depth** | The pair's full listing history | 3,528 bars back from today, per timeframe (table below) | 3,528 bars, on a 24x5 session | 3,528 bars, on a 24x5 session | | **Default cost** | 0.05% fee + 0.02% slippage | 0.01% fee + 0.02% slippage | 0.01% slippage, no commission | 0.015% slippage, no commission | | **Funding, open interest, basis** | Yes | No | No | No | | **Stops in pips** | No | No | **Yes** | No | | **Overnight swap** | Not applicable | Not applicable | **Modelled** | Not modelled | **What each "no" actually means.** Open interest and funding are read from Binance, so there is no series to read on a stock or a currency pair. A pip is defined for a currency pair and nowhere else: brokers quote gold in both 0.01 and 0.1 and call either one a pip, so a stop in pips on gold is refused by name rather than guessed at. Metals accrue no swap because no lease rate series exists to compute one from, and treating it as zero would assert that gold has no carry, which is false. Forex swap is modelled from central bank and overnight interbank rates for 15 currencies, charged per rollover night with the usual Wednesday triple. **CNH, SGD and HKD trade but accrue no swap**, because no overnight rate series is published for them; the run says so rather than charging zero quietly. ## How far back stock data reaches Equities, forex and metals are served by the TradingView oracle, and it reaches back a **fixed number of bars from today** per symbol and timeframe: **3,528**. So the depth is counted in bars, and what that buys in time depends on the timeframe. On the 390 minute US equity session: | Timeframe | Bars per day | History for 3,528 bars | | --- | --- | --- | | 1m | 390 | 9 trading days | | 3m | 130 | 27 trading days (about 5 weeks) | | 5m | 78 | 45 trading days (about 2 months) | | 15m | 26 | about 6.5 months | | 30m | 13 | about 1 year 1 month | | 45m | 9 | about 1 year 7 months | | 1h | 7 | 2 years | | 2h | 4 | 3.5 years | | 4h | 2 | 7 years | | 1d | 1 | 14 years | A stock run whose range starts before the reach is refused **before a credit is spent**, with code `DATA_HISTORY_REACH`, the earliest date the data covers, and the two ways out: start the range on or after that date, or use a higher timeframe for a longer one. It is a data limit, the same on every plan, and it does not apply to crypto, where exchange history reaches the pair's listing. Forex and metals trade 24 hours a day, five days a week, so the same 3,528 bars cover a shorter calendar on intraday timeframes (about 7 months on 1h) and the same 14 years on daily. The reach says how far back the data exists. The bar ceilings above say how much of it one run may span. You meet whichever you hit first. Premarket and after hours prints are not in the equities feed at all, so a strategy cannot trade them. Egypt (EGX) is supported alongside US equities, with its own holiday tolerance. ## Writing your own indicator Four ways, and they are not interchangeable. The gate is per plan; see the [pricing page](/pricing) for which tier carries which. | | Pine Script | JavaScript | Python | CSV upload | | --- | --- | --- | --- | --- | | **Runs where** | TradingView, via our oracle | Our sandbox | Our sandbox | Nowhere, the values are already computed | | **Pinned to a symbol/timeframe** | **Yes**, recompiled per symbol | No | No | **Yes**, to the file's range | | **Works on every asset class** | Yes | Yes | Yes | Yes | | **Time budget** | Oracle round trip | 3 seconds | 3 seconds | Not applicable | | **Memory** | Not applicable | 64 MB | 64 MB | Not applicable | | **Size limit** | 100,000 characters | 128 KB | 128 KB | 5 MB, 100,000 rows | | **Plots / inputs** | Pine's own | 12 / 24 | 12 / 24 | One column each | | **Bars per run** | 350,000 | 350,000 | 350,000 | 350,000 | **Pine goes through TradingView on every asset class, including crypto.** Crypto candles come from Binance, but a Pine indicator in that run is still compiled by the oracle against the same symbol, so it spends the shared request budget that crypto data itself never touches. A long crypto backtest with a Pine indicator is slower than the same backtest with a built in one for exactly that reason. JavaScript and Python are the same capability with the same helpers and the same numbers. Neither is pinned to anything: the saved indicator recomputes against whatever bars a run uses, so one indicator works on every symbol and every timeframe. Neither can reach the network, read the clock, or use randomness, so a backtest reproduces. See [JavaScript](/docs/reference/javascript-indicators) and [Python](/docs/reference/python-indicators). A CSV carries values you computed elsewhere. It is aligned onto the run's bars by timestamp, and it covers only the dates in the file, so using it on another symbol or a later range means uploading again. ## Portfolios A book holds one asset class and one session calendar. A mixed book is refused, naming both sleeves, because two session grids and two annualisation bases cannot share one equity curve. The v1 portfolio path is spot crypto, up to 20 assets; see [Portfolios](/docs/reference/portfolio). The portfolio builder in the terminal is an Enterprise feature; Power and Quant run portfolios through a connected AI agent. ## What the engine will not do at all - **No premarket or after hours equities data.** The feed carries regular hours bars only. - **No trading calendar lookup.** Holidays are inferred from gaps in the data, tolerated up to 4.5 days for US venues and 11 for Egypt. - **No broker specific spreads or swap rates.** Every cost applied when your query does not state one is a documented default, labelled `assumed` on the run. - **No regime attribution below 5 trades or 60 bars.** The run says why instead of showing an empty panel. Related: [Markets & data](/docs/reference/markets), [Costs & fees](/docs/concepts/costs), [Plans & credits](/docs/concepts/plans-and-credits). --- ## Operators & conditions Source: https://docs.texttoquant.com/reference/operators Summary: Comparisons, crosses, ranges and boolean logic. Operators are how a query compares one value to another: an indicator to a number, a series to another series, or a price to a level. Crosses compare against the previous bar; percentile ranks compare against trailing history. ## Comparison & event operators | Operator | Meaning | | --- | --- | | above / below | Stative compare vs a value or another series | | at or above / at or below | Inclusive compare (`>=` / `<=`) | | equals / not equal to | Exact match / mismatch, e.g. a flag `== 1` | | between / outside | Inside or outside a range, e.g. `RSI between 40 and 60` | | crosses above / below | Event; compares against the previous bar | | rising / falling | Series turning up / down | | reaches | Price attains a dynamic level (TP / SL) | | retests | Returns to a level after breaking through it | | touches | Hits a band or level | | deviates above / below | Moves X% or X ATR from a reference | | divergence bull / bear | Price pivot vs an oscillator pivot: RSI, MACD, OBV, volume, CCI, MFI, ROC, Williams %R or stochastic | | stays idle | Level unchanged for N bars | A "cross above" is an *event*: it fires only on the bar where the series moves from below to above. A "stays above" is a *state*: it's true on every bar where the condition holds. ## Advanced operators Beyond the basics, the parser understands a range of specialised comparisons. | Operator | Example | | --- | --- | | within X% of | `price within 0.5% of the VWAP`, proximity to a level or series | | for N bars | `RSI stays above 50 for 3 consecutive bars`, a state must persist | | present / absent | `a buy signal prints`, `no sell signal`, a marker fires, or doesn't | | inside / outside zone | `price is inside a bullish order block`, `price leaves the fair-value gap` | | zone formed | `a new fair-value gap forms`, a supply/demand zone just appeared | | expansion bar | `the bar range expands beyond 1.5× its recent average` | | gaps up / down X% | `price opens 2% above the previous close` | | N of the last M | `3 of the last 5 closes are above the VWAP`, `a majority of the last 10 candles are green` | | the Nth time | `the 2nd time RSI crosses above 30`, pick a specific occurrence | | within the last N bars | `RSI crossed above 30 within the last 10 bars`, optionally `at least K times` | The zone operators (`inside`/`outside`/`formed`) work on structure sources: fair value gaps, order blocks, breakers, liquidity sweeps, and read a zone's freshness (fresh, tested, mitigated), so "price taps an *unmitigated* order block" is expressible. See the structure indicators in the [indicator library](/docs/reference/indicators). ## Expression form For comparisons the flat operators can't express, rolling windows, N bars ago, multiples, or any indicator as an operand: | Building block | Example | | --- | --- | | Rolling window | `the 20-bar highest high`, `20-bar mean volume` | | N bars ago | `RSI vs its value 5 bars ago` | | Multiple | `volume above 2× its 20-bar average` | | Aggregation | `highest`, `lowest`, `mean`, `sum`, `std` or `median` over a rolling window | | Value when | `the RSI value the last time price crossed the 200 EMA` (value when) | | Indicator operand | `EMA of RSI`, `MACD histogram vs 0` | ## Combining conditions Join conditions with `AND` (every one true on the same bar) or `OR` (any one true). To require that one condition happens *after* another, chain them with `then`. That makes a **sequential** entry: the steps must fire in order, not all at once. ```text Buy when RSI crosses above 50 AND price breaks the swing high Buy when the 12 EMA crosses above the 21 EMA, then price retests the 21 EMA ``` | Logic | Meaning | | --- | --- | | `AND` | every condition is true on the **same** bar | | `OR` | **any** condition is true | | `then` (sequential) | conditions fire **in order**, each within a window of the previous step | ### Sequential timeout A sequential entry puts a deadline on each step: the next condition must occur within a set number of bars of the one before it, or the sequence resets and starts watching for step one again. The default window is **50 bars**. Set your own with `within N bars`. ```text Buy when the 12 EMA crosses above the 21 EMA, then price retests the 21 EMA within 20 bars ``` In a sequential entry the conditions are matched strictly in the order you write them. Reordering the clauses changes the strategy; `AND` does not. ## Beyond a plain comparison A lot of ideas aren't shaped like "value vs number": where price sits in its 52 week range, how many bars since a signal, whether a state held for five bars, relative volume, a move measured in ATR. Those are documented as their own reference, see [phrasing families](/docs/reference/phrasings) for the exact wording and the maths behind each one. See [entry & exit](/docs/reference/entry-exit) for how conditions, stops and sizing come together. --- ## Phrasing families Source: https://docs.texttoquant.com/reference/phrasings Summary: Ways of saying a condition beyond a plain comparison: range position, streaks, recency, relative volume and volatility scaled moves. Most conditions are a plain comparison: a value against a number or another series. But a lot of real trading ideas are not shaped like that. They talk about **a window**, where price sits in its yearly range, how long ago something happened, whether a state held for five bars, how big a move is *relative to* current volatility. Each of those is a **phrasing family**: a way of saying a condition that gets compiled into exact maths on your bars. They are not a special mode or a separate syntax, write them in an ordinary strategy sentence and combine them freely with everything else (AND/OR, direction, sessions, higher timeframe gates, stops and targets). Each example below is a working phrase. The wording in **bold** is what the parser keys on, keep it and swap the asset, the numbers and the window. Every family has a ready to run card in the [templates gallery](/templates) under **Windows & streaks**. ## Where a value sits in a window | Say this | What the engine checks | | --- | --- | | price is in the **bottom 20% of its 52 week range** | `close ≤ lowest(low, N) + 0.20 × (highest(high, N) − lowest(low, N))` | | price trades in the **top 10% of the 20 day range** | the same band, measured from the top | | price is in the **lower quartile of its 100 bar range** | quartile / third / half / decile all work | | price is **20% below its all time high** | `close ≤ 0.80 × all-time high` | | price is **within 5% of the 52 week high** | proximity to one extreme | The window is read on **your** timeframe: a 52 week range on the 1d is 364 bars; a 6 month range on the 4h is 1080. "20% below the high" anchors on **one** extreme. "The bottom 20% of the range" anchors on the **whole** range: high *and* low. In an asymmetric range these are different conditions, and the parser keeps them apart. Say the one you mean. ## Where the bar closed inside itself A close strength (close location) filter, for when a bar closing on its high means something different from a bar closing on its low. | Say this | What the engine checks | | --- | --- | | the candle **closes in the top 25% of its range** | `close ≥ low + 0.75 × (high − low)` | | price **closes in the lower third of the bar** | `close ≤ low + 0.333 × (high − low)` | | the bar **closes near its high** | the top quarter of the bar (the documented convention) | The deciding difference from the family above is the window: **a bar/candle range** (or no window at all) is this one; **an N day / N week / N bar range** is range position. ## How long ago something happened | Say this | What the engine checks | | --- | --- | | **at least 10 bars since** RSI was above 70 | `bars since (RSI > 70) ≥ 10` | | **fewer than 5 bars since** the MACD crossed its signal | `bars since (cross) < 5` | | **more than 20 bars since** price closed above the 200 EMA | `bars since (close > EMA200) > 20` | "10 bars since RSI was above 70" does **not** mean RSI is above 70 now, it means it isn't, and hasn't been for a while. Use `at least / more than` for a cooldown and `fewer than / within` for freshness. ## A state or a direction that has to persist | Say this | What the engine checks | | --- | --- | | RSI has been **above 50 for 5 consecutive bars** | the comparison holds on every one of the last 5 bars | | price has **stayed above the 20 EMA for 3 bars in a row** | same, against a series | | the 50 EMA has been **rising for 5 consecutive bars** | the EMA is higher than the bar before on each of those 5 steps | | RSI has been **falling for 3 bars** | the mirror | Compare with the *proportion* form, which is a different question: **60% of the last 20 bars** closed above the 50 EMA counts bars in a state; **for 5 consecutive bars** requires all of them. "the 10 bar ROC is **positive**" is a level (`ROC > 0`). "the 10 bar ROC is **rising**" is a direction. Adding "for 3 bars" to either one makes it persist; it does not turn one into the other. ## Volume, normalised Raw volume thresholds don't travel between assets or across years. These do. | Say this | What the engine checks | | --- | --- | | **relative volume is above 2** (or **RVOL > 2**) | `volume ≥ 2 × mean(volume, 20)`, twice normal participation | | relative volume above 1.5 **over the last 50 bars** | the same, averaged over 50 bars | | **dollar volume is above $10 million** | `close × volume ≥ 10,000,000` | | **turnover** above $5M | same as dollar volume | | volume is **twice its 20 bar average** | `volume ≥ 2 × mean(volume, 20)` | "relative volume above 2" means twice the average, not two units, which would be true on every bar. If you want an absolute floor, say `volume is above 1,000,000`. ## A series at its own extreme For any nonprice series (RSI, ADX, ATR, volume, MACD) this adapts instead of waiting for a fixed level a strong asset may never reach. | Say this | What the engine checks | | --- | --- | | RSI is **at its lowest in 50 bars** | `RSI ≤ lowest(RSI, 50)` | | volume is **the highest of the last 20 bars** | `volume ≥ highest(volume, 20)` | | ADX is **at a 100 bar high** | `ADX ≥ highest(ADX, 100)` | For **price** extremes, use the breakout wording instead: `price makes a new 20-day high`, `price breaks above the 20-bar high`, which routes to the breakout/Donchian logic. ## Moves measured in volatility The same percentage means something different in a calm month and a violent one. Measuring in ATR keeps the rule honest across regimes. | Say this | What the engine checks | | --- | --- | | price has **fallen more than 2 ATR in the last 5 bars** | `close − close[5] ≤ −2 × ATR(14)` | | price has **risen more than 1.5 ATR(20) over the last 10 bars** | with an explicit ATR period | | price has **moved more than 3 ATR over the last 10 bars** | undirected, both tails fire | | price closes **more than 2 ATR above the previous close** | the single bar form | | price is **2 ATR below the 20 EMA** | an overextension band around an indicator | ## Intraday anchors: the opening range and today's levels These read the CURRENT day, and reset at each new day. | Say this | What the engine checks | | --- | --- | | price **breaks above the high of the first 30 minutes** | the first N bars range of the day, broken | | price **breaks below the low of the first 4 bars** | the same range, downside | | price is **above today's open** | `close > day_open` | | price hits **the day's high** / **the low of the day** | the running extreme so far today | | price is **down more than 3% on the day** | `close ≤ 0.97 × day_open` | "Today's high" is the running extreme **so far today**; "yesterday's high" is a completed previous day level. They are different conditions, and the parser keeps them apart. And if you name a **session**, "the New York session opening range", that anchors to the session open instead of the day open, which is a separate (also supported) level. Until the first N bars of the day have printed, the range is undefined and the condition is false. That is deliberate: a breakout judged against a range still being built would be reading bars the strategy has not traded through. ## Gaps, coils and bar shape | Say this | What the engine checks | | --- | --- | | price **fills yesterday's gap** / **the gap closes** | price trades back through the previous day's close | | the **last 10 bars have a range under 3%** | `highest(high,10) − lowest(low,10) ≤ 0.03 × close` | | the **20 bar range is tighter than 5%** | the same, wider window | | the **candle range is more than 1.5 ATR** | `(high − low) ≥ 1.5 × ATR(14)` | | the **body is bigger than 1 ATR(20)** | `\|close − open\| ≥ 1 × ATR(20)` | | price is **within 0.1% of a round 1000 level** | distance to the nearest multiple of 1000 | "Fills the gap" says nothing about which way the gap went, so the condition fires when price touches the previous close from either side. Say "gaps down 2%" separately if you want to detect the gap itself as well. "A round number" could mean 100, 1000 or 10000, the parser will ask rather than guess, so say which ("a round 1000 level", "multiples of 500"). And because the tolerance is a percent of price, it must be tighter than half the increment: at BTC prices, "within 1% of a round 1000 level" is wider than the gap between levels, so every bar qualifies. ## Ranking a series against its own history | Say this | What the engine checks | | --- | --- | | RSI is in the **bottom 10% of its last 100 readings** | percent rank of RSI over 100 bars ≤ 10 | | ATR is in the **top 5% of the last 200 bars** | percent rank ≥ 95 | | volatility is **in its bottom quartile** | the regime filter (no explicit window needed) | The explicit "of its last N readings" form works **in exits too**, and composes inside larger expressions, the shorter regime phrasing is an entry filter. ## How this asset moves against another one These are relationships, not filters. "Only while BTC is above its 200 EMA" asks about BTC; these ask how the two assets move **together**. Any condition that reads another market is on Power and above (and the SPY rows also need stock data, which starts at Power). | Say this | What the engine checks | | --- | --- | | the **30 day correlation between ETH and BTC** drops below 0.5 | correlation of the two **return** series over 30 bars | | the **correlation to BTC** is above 0.8 | the same, against the asset you are trading | | ETH has **outperformed BTC over the last 30 days** | ETH's 30 bar growth ≥ BTC's 30 bar growth | | it is **underperforming SPY** over 60 bars | the mirror | | ETH **outperforms BTC by more than 5%** over 20 days | with an excess return margin | | the **ETH/BTC ratio is at a 50 bar high** | the pair ratio at its own rolling extreme | | the **60 day beta to SPY** is above 1.5 | the OLS slope of this asset's returns on the benchmark's | Two assets that both trend up have a price correlation near 1 no matter how differently they behave, so a price based answer would be meaningless. These conditions compare the **return** series, which is what the words mean to a trader. "Beats the market" and "correlated with everything" have no series behind them, so the parser will not invent one. Say the symbol: `outperformed BTC`, `beta to SPY`, `correlation between ETH and BTC`. ## Exits that depend on the trade, not on a price Most exits are a level, a stop, a target, an indicator crossing. These four depend on how the trade itself has behaved, which no price condition can express. | Say this | What the engine does | | --- | --- | | **exit if the trade isn't profitable after 10 bars** | at bar 10 onward, close on the first bar that is not in profit | | **give it 20 candles to work** then cut it | the same, in the other phrasing | | **exit if it gives back half of the open profit** | close once the open profit falls to half its peak | | **close it if it retraces 30% of the gain** | the same, at 30% | | **exit on the first profitable close** | close on the first bar strictly beyond your entry | | **hold for a maximum of 5 days** | a hard time cap, converted to bars for your timeframe | | **exit when price closes below the lowest low of the last 5 bars** | a structure trail | "Exit after 10 bars" closes the trade no matter what, winners included. "Exit if it isn't profitable after 10 bars" spares the winner and cuts only the trade that failed to work. Both are supported; they are different strategies, so say the one you mean. A trailing stop follows price by a fixed distance. A give back is a fraction of the profit the trade has already shown, so it tightens automatically as the winner grows. It also arms only once the trade has genuinely been green, the peak is measured on closes, so a wick through your entry cannot turn it into a second stop loss. The engine's clock is bars. "A maximum of 5 days" on a 4 hour chart becomes 30 bars, but only because you said *days*. If you write "5 bars", you get 5 bars. ## This week, this month, this year The day anchors have week, month and year siblings. These are the **current** period's running values, and they reset at each boundary. | Say this | What the engine checks | | --- | --- | | price breaks above **this week's high** | the running high of the current week | | price hits **the low of this month** | the running low of the current month | | BTC is **down more than 5% this week** | `close ≤ 0.95 × the week's open` | | it is **up 20% year to date** | `close ≥ 1.20 × the year's open` | | price **makes a new weekly high** | this bar takes out the week's high so far | | price is in the **bottom 20% of this week's range** | where in the week's range it is trading | "This week's high" is the running high of the week you are in; "last week's high" is a completed level from a different week. Both work, they are different conditions, and the parser keeps them apart on exactly that wording. "Down 5% this week" is measured from the week's open and starts again every Monday. "Down 5% over the last 10 bars" slides forward every bar and never resets. Pick the one that matches how you think about the trade. ## Two timeframes in one condition Saying "the daily RSI is above 50" moves that whole condition to the daily chart, which is usually exactly right. These three are the cases where the two **sides** of the comparison live on different timeframes, which a single higher timeframe filter cannot express. A condition that reads another timeframe, including the daily filter above, is on Power and above. | Say this | What the engine checks | | --- | --- | | the **1h RSI is above the daily RSI** | this chart's RSI vs the daily RSI, in one condition | | price is **more than 3% below the daily 200 EMA** | the *current* price against a daily level | | price is **2 ATR above the weekly VWAP** | the same, measured in volatility | | price is **above where it closed 3 daily bars ago** | this chart's price vs a daily close from three days back | "Price is above the 200 EMA on the daily" asks whether the *daily* bar is above its daily EMA, a trend filter, and the right reading. "Price is 3% below the daily 200 EMA" asks how far the price is *right now* from that level. Adding the distance is what tells the parser you mean the second one. "Where it closed 3 daily bars ago" is an earlier value of the same series. `RSI(3)` is a different indicator entirely. The parser keeps them apart, but the wording matters. ## Who is actually doing the buying | Say this | What the engine checks | | --- | --- | | the **cumulative volume delta over the last 20 bars is positive** | each bar's volume signed by whether it closed up or down, summed | | **CVD turns negative** over 50 bars | the same, other side | | **up volume is more than twice down volume** over the last 20 bars | the two sides summed separately and compared | | **dollar volume is in the top 10% of the last 100 bars** | participation ranked against its own history | | the **bar range is in the top 5% of the last 200 bars** | the same, on range | | bullish **OBV divergence**, bearish **volume divergence** | divergence measured on that series | Divergence works on **obv, volume, cci, mfi, roc, williams %r and stochastic** as well as rsi and macd. If you were told otherwise before, that guidance was out of date. True cumulative volume delta needs tick data to know which side each trade hit. On bars, the standard approximation is each bar's volume signed by whether it closed up or down, which is what these conditions use. It is a good proxy, not the real thing. ## Volatility and regime These describe how the asset is *behaving* right now, rather than where its price is. They are unit free, so the same number means the same thing on every asset and in every year. | Say this | What the engine checks | | --- | --- | | **20 day realized volatility is below 40%** | the standard deviation of returns, annualised, as a percentage | | **historical volatility is above 80%** | the same (window defaults to 20) | | the **20 bar efficiency ratio is above 0.6** | how much of the distance travelled actually went somewhere | | a **3 standard deviation down move** | the size of the return against the asset's own recent spread | | the move is **more than 3 sigma** | the same, either direction | | the **20 day autocorrelation is negative** | whether yesterday's move tends to be given back | | returns are **positively autocorrelated** over 30 days | the same, other side | "Volatility below 40%" and "ATR below 40" are different conditions. ATR is measured in the asset's own currency, so 40 means something different for every asset and every year. Realized volatility is the annualised percentage every options desk quotes, write the **%** and you get the comparable one. Both answer "is this trending or chopping". The efficiency ratio is bounded between 0 and 1: 1.0 is a straight line, 0 is noise that ends where it started, so a threshold you pick on one asset transfers to another. ADX does not have that property. **"2 standard deviations below VWAP"** is a *band* around VWAP, not the size of a move, the sigma points at a reference series. **"a 2 standard deviation down move"** is the size of the return. Both work; naming the reference series is what tells them apart. ## Candle geometry Named candlestick patterns already work: **hammer, doji, engulfing, morning star, marubozu, shooting star** and around sixty more. Name one and you get it. These phrasings are for when you'd rather describe the *shape* than name the pattern. | Say this | What the engine checks | | --- | --- | | a **long lower wick**, a **long tail** | the lower shadow is at least twice the body | | the **upper wick is at least 60% of the range** | that shadow against the whole bar | | the **lower wick is more than twice the body** | the multiple you state | | the **body is more than 70% of the range** | how much of the bar the body fills | | a **wide bodied candle** | the same, at 70% | | the **body is the largest of the last 10 bars** | against the ten bars *before* this one | | an **inside bar**, an **outside bar**, an **outside reversal** | this bar's range against the previous bar's | | **gaps up more than 2%**, **opens 1% above the previous close** | the open against the previous close | "A long lower wick" and "a hammer" select **different bars**. A hammer additionally requires a prior downtrend and a small upper wick, so naming the pattern narrows the strategy. Say whichever one you actually mean, both work. An **outside bar** takes out both extremes of the previous bar, it compares full ranges. A **bullish engulfing** compares bodies. Different conditions, both supported; say the one you want. A gap is the open against the previous close, so it only exists where trading stops overnight, **stocks**. On 24/7 crypto the tape is continuous and every bar opens where the last one closed, so there is nothing to measure. Ask for a gap on a crypto pair and the phrase is read the way a crypto trader means it: the bar **closed** that far above the previous close. ## Volatility, persistence and shape | Say this | What the engine checks | | --- | --- | | **ATR is above 3% of price**, **ATR% below 1.5** | ATR divided by price, as a percentage | | RSI has been above 50 **for the last 3 days** | every bar in those three days | | price **stayed above the 200 EMA for two weeks** | the same, in weeks | | **three consecutive narrowing bars**, a **coil** | each bar's range against the one before it | | the range has **expanded two bars in a row** | the mirror | | the 50 and 200 EMA are **converging**, **pulling apart** | the gap against its own value a few bars ago | **"ATR above 3"** and **"ATR above 3% of price"** are different conditions. ATR is quoted in the asset's own currency, so a bare number is a *price*: on a $60,000 asset, "ATR above 3" is three dollars: true on every bar. It also drifts as price moves, so a threshold tuned at one price stops working at another. Say the percentage and the filter travels. A persistence filter can be written in calendar time: "for the last 3 days", "for two weeks", and the timeframe is accounted for: three days is **eighteen** bars on a 4h chart and **three** on a daily one. You don't need to do the conversion. Two averages can be far apart and closing fast, or nearly touching and drifting apart: opposite trades. **"Converging"** asks whether the gap is *shrinking*; ask for the spread if you want how far apart they are right now. ## Everyday conditions | Say this | What the engine checks | | --- | --- | | the **third touch** of the 200 EMA, the **second retest** of VWAP | the level falls inside the bar, counted | | only when the **daily candle is green** (Power) | the last completed daily close against its open; reading another timeframe is Power and above | | while the **4h candle is red** (Power) | the same, other side | | price is **within 1 ATR of the 50 EMA** | the gap measured in ATR, not percent | | **stop 1 ATR below entry**, **take profit at 3 ATR** | a stop sized to the asset's own volatility | **"Stop 1 ATR below entry"** and **"1% stop"** are very different orders. Write the word **ATR** and the stop is sized to volatility; write **%** and it is a fixed percentage. You can mix them in one strategy, "3 ATR target and a 2% stop" keeps each one as written. **"The third touch"** is not the same request as **"a touch"**, it fires on different bars and far less often. A touch means the level fell inside the bar's range, so a bar that merely closed nearby does not count. "Only when the daily candle is green" on a 1h chart reads the last **finished** daily candle. The day still forming is not used, because its colour is not knowable at the time the trade would be taken. ## The rest of the family list These work the same way and are documented alongside the operators: | Say this | What it means | | --- | --- | | price is **within 2% of the 200 EMA** | proximity to a level | | the bar's **range is twice the average range** | range expansion | | the **z score is below −2** | standardised distance from the mean | | the **50 EMA is more than 5% above the 200 EMA** | percent spread between two indicators | | **RSI rose 20 points over the last 5 bars** | change of a series over a window | | RSI is **2 standard deviations below its 20 bar mean** | a statistical band on any series | | volatility is **in its bottom quartile** | percentile rank of a series vs its history | ## If a phrase doesn't work Two things help more than rewording blindly: 1. **Check the parse.** The terminal shows the parsed conditions before you run, if a clause is missing or reads differently from what you meant, that's the signal. 2. **Use the wording above verbatim,** then change the numbers. The examples are the exact phrasings the parser is trained and tested on. See also [operators & conditions](/docs/reference/operators) for the comparison vocabulary, and [query syntax](/docs/reference/query-syntax) for the anatomy of a full strategy sentence. --- ## Entry & exit logic Source: https://docs.texttoquant.com/reference/entry-exit Summary: How entries, exits, stops and targets are evaluated. A trade has two halves: the conditions that open it, and everything that closes it, exit conditions, stops, targets and trailing logic. This page covers both, plus how you size the position and bring in custom data. ## Entry & exit conditions Entries and exits are built from indicator, price and pattern conditions. Combine them with `AND` / `OR`, or run them in sequence with `then`. ```text Buy when RSI crosses above 50 AND price breaks the swing high, for BTC 1D ``` ```text Buy BTC when the 12 EMA crosses above the 21 EMA, then price retests the 21 EMA, 4H last 90 days ``` ```text Buy BTC when RSI is higher than it was 5 bars ago, exit when it's lower, 4H last 90 days ``` The `then` in the second query makes the entry **sequential**: the EMA cross must come first and the retest must follow, in that order, not on the same bar. Each step waits up to 50 bars for the next one by default; add `within N bars` to set the window, or the sequence resets. See [combining conditions](/docs/reference/operators#combining-conditions). ## Stops & targets | Control | Example | | --- | --- | | Stop loss | `SL 2%`, `SL 1.5 ATR`, `stop 1R`, at entry candle low | | Take profit | `TP 5%`, `TP 3RR`, `take half at 2%` | | Trailing | `trail by 1.5 ATR`, `trailing stop 3% (activate at 1%)` | | Stop plan | `move to breakeven at 1R, then trail 1 ATR at 2R` | | Partial in / out | `partial exit 50% at 1R, remainder at 2R` | | Channel exit | `exit on a new 20-bar low`, close on a new N bar high/low (Donchian style) | ## Position sizing | Mode | Example | | --- | --- | | Risk based | `risk 1% of equity per trade with a 2% stop` | | % of equity | `use 10% of equity per trade` | | Fixed units | `0.05 BTC per trade` | | Fixed notional | `$5,000 per trade` | | Leverage | `3× leverage` | ## Order execution By default an entry fills **at the close of the signal bar** (see the [execution model](/docs/concepts/execution-model)). You can also rest a **limit order**: it waits for price to come to a level and fills on a later bar if it's touched, expiring after a set number of bars. | Mode | Example | | --- | --- | | Market (default) | fills at the signal bar's close | | Limit | `place a limit buy at the 20 EMA, expire after 5 bars` | The engine models **market** and **limit** fills only. Phrasings that imply a next bar open or a stop entry order resolve to the market fill, the docs don't promise an order type the engine doesn't run. ## Event windows A single condition can require that something happened **recently**, not necessarily on this exact bar: "X within the last N bars", optionally "at least / at most K times". This widens *when a condition counts as met*: distinct from the sequential `then within N bars`, which orders two separate steps. ```text Buy BTC when RSI crossed above 30 within the last 10 bars, 4H last 90 days ``` ## Performance gates The strategy's own live performance is available as a condition, just like an indicator. Five runtime metrics are computed bar by bar from the equity curve of the run itself, so a query can gate its entries on how the strategy has been doing: | Metric | Meaning | | --- | --- | | `rolling performance` | % return of the strategy over the last 30 bars | | `rolling drawdown` | % of equity below its 30 bar peak (0 at highs, negative under water) | | `rolling sharpe` / `rolling sortino` | Annualized risk adjusted return over the last 30 bars | | `equity` | Current account equity in $ | ```text Buy BTC when RSI crosses above 50, but only trade if performance is above 0%, 4H last 180 days ``` ```text Buy when price breaks the swing high, only if drawdown is better than -10%, for ETH 1D ``` For hard cutoffs rather than bar by bar gates, use the risk guards: `stop trading if drawdown exceeds 20%`, `stop after 3 consecutive losses`, `stop for the day after losing 5%`, `max 2 trades per day`, `wait 5 bars between trades`. While a gate blocks entries the equity curve goes flat, and after 30 flat bars `rolling performance` settles at exactly 0. A strictly positive threshold (`above 0%`) then never rearms and the strategy stays shut off for the rest of the run. Give the gate a way back in: use a slightly negative threshold (`above -2%`), or gate on drawdown, which rearms as the window rolls past the old peak. Runtime metrics track the strategy's actual equity, including only trades that were really taken. The rolling window is fixed at 30 bars of the query's timeframe. They are not a per signal track record: "only take this signal if it was profitable the last 3 times it fired" is a different concept and isn't part of a gate. To study a single condition's historical outcomes, use the probability scan in [analysis](/docs/reference/analysis). ## Custom indicators & @tags Bring your own series from CSV columns or Pine. Tag them with `@"Column"` and align the timeframe in the UI. You can combine several custom series in one query. ```text Long when @"fast" crosses above @"slow", SL 2% TP 3RR, for BTC 4H last 60 days ``` When strict mode is on, a custom query requires `@` tags, a data source, and a parsed custom condition, so a typo can't silently fall back to a built in indicator. --- ## Portfolios Source: https://docs.texttoquant.com/reference/portfolio Summary: Run several assets from one shared capital pool: rosters, contention, rebalancing, risk controls, deposits and withdrawals, the book report, and the known limits behind a surprising zero. A **portfolio** runs several assets from **one shared pool of capital** as a single *book*. Each asset carries its own strategy, and a **contention** rule decides who gets the cash when more than one asset wants to enter on the same bar. Where a single backtest is one symbol, one strategy and its own balance, a portfolio is a roster of assets competing for one pool, with a shared equity curve, per asset attribution, and a record of every signal that could not be filled. The v1 portfolio path is spot crypto only. Symbols resolve to Binance USDT pairs (BTC becomes BTCUSDT). A book holds up to 20 assets. The portfolio builder in the terminal, with its Single asset and Portfolio toggle, is Enterprise only. Power and Quant run portfolios through a connected AI agent over [MCP](/docs/api/mcp). ## Building a book There are three ways in, and they all land on the same review then run flow. For a step by step walkthrough see [Build a portfolio](/docs/guides/build-a-portfolio). | Path | How | | --- | --- | | One prompt | Describe every asset in a single sentence. A multi asset prompt is split into one parsed strategy per asset, and the terminal switches to Portfolio mode for you. | | Visual builder | Flip the sidebar toggle from **Single asset** to **Portfolio**, open the **Visual** tab, and add assets by hand. Each asset opens the normal strategy builder canvas. | | API / MCP | `parse_portfolio` then `run_portfolio`, see the [MCP server](/docs/api/mcp). | ```text Buy BTCUSDT and ETHUSDT on 1d when price closes above the 20 EMA; exit when it closes below. $50k, prorata. ``` Each asset shows up as a review card with the clause the splitter assigned to it ("Split as"), its parsed logic, and its market. Before you run, you can edit any asset, copy one asset's rules to the whole roster, or remove it. ## Shared capital & contention A book trades from one **initial capital** amount, not one balance per asset. The terminal starts a new book at **$30,000**; the API and MCP default to **$100,000**. When two or more assets signal an entry on the same bar and there is not enough free cash for all of them, the **contention** rule resolves the conflict: | Rule | Behaviour | | --- | --- | | `rank` (default) | Priority order wins. Priority follows roster order unless you set it, say "prioritise BTC over ETH" to pin it. | | `prorata` | The contested cash is split proportionally across the competing assets. | | `strength` | The strongest signal fills first. Ask for it with "strongest signal fills first". | A signal that cannot be funded is never dropped silently. It is written to the **skip ledger** with a reason, see [Reading the book report](#reading-the-book-report). ## Rebalancing Off by default. You turn it on by naming target weights; percentage stops and take profits are never mistaken for weights. | Setting | Values | | --- | --- | | Schedule | daily, weekly, monthly, quarterly, yearly, or **never** (default monthly) | | Overlay mode | resizes open positions toward the target weights on each boundary, while entry signals still fire | | Allocation mode | holds the whole book to the target weights with no entry signals, a long only hold to weight book | | Targets | per asset weights that sum to 100% or less; a cash remainder is allowed | | Cash sleeve | state the remainder explicitly: "hold 40% BTC, 30% ETH and 30% cash" | | Drift band | rebalance off calendar whenever a sleeve drifts this far from target. Quote it in **percentage points** or in percent: a band is a difference between two percentages, so "5 points off target" and "a 5% drift band" are the same rule | | Minimum trade | skip corrective trades below a dollar amount, so a tight band stops generating $12 trades | ```text Rebalance monthly to 40% BTC, 30% ETH, 30% SOL. Hold 60% BTCUSDT and 40% ETHUSDT, rebalance quarterly. Allocate 50% BTCUSDT and 50% ETHUSDT once and never rebalance. Hold 60% BTCUSDT and 40% ETHUSDT, rebalance monthly but skip rebalance trades under $500. Equal weight BTCUSDT, ETHUSDT and SOLUSDT, rebalance monthly and also whenever a position drifts more than 5 points from its target. ``` Every rebalance is turnover you pay for. Quarterly tracks the targets more loosely than monthly and trades about a third as often; **never** allocates once on the first bar and then lets the winners run, which is the buy and hold baseline every other schedule should be measured against. ### Rotation Instead of fixed weights, rank the universe each rebalance and hold only the leaders, or the laggards. ```text Buy the top 3 by momentum over 30 days, rebalance weekly. Hold the 2 lowest volatility coins, rebalance monthly. Rotate into the best 2 by 30-day return and short the worst 2. ``` Rank by momentum, return, rate of change, RSI, Sharpe or volatility. A rotation is always an allocation book, the held set and the weights are both chosen by rank, so entry signals do not run. Ranking is **relative**: it answers *which of these is best*, never *is any of them any good*. In a market where everything is falling, a top N rotation dutifully buys the best of the losers and stays fully invested all the way down. Add the absolute test and the book goes to cash instead when nothing clears it: ```text Hold the top 2 by 90 day momentum but only while their momentum is positive, rebalance monthly. Rotate into the top 3 by momentum, cash if none are positive. ``` That second test is what makes it *dual* momentum. The floor gates the LONG side only: it says which names are worth owning, not which are worth shorting, so a long/short rotation still takes its short sleeve from the worst ranked names. When nothing clears the floor the run says so explicitly, so a book that deliberately sat out a bear market is never mistaken for a broken rotation. ## Risk controls All optional, and all off unless you ask for them. State them once, anywhere in the prompt, a control that talks about the whole book never has to be repeated per asset. Percentages that belong to a leg ("stop loss 3%") are never mistaken for a book knob. Every control below can be **typed in the prompt** or **edited in the Book settings panel** on the review page: including the ones whose value is a ladder, a schedule, a list of days or a session window. Clearing an editor turns that family off completely; there is no "set but inert" state. An edit you make by hand is authoritative: reparsing the prompt afterwards will not overwrite it. ### Size and concentration | Control | Effect | Say | | --- | --- | --- | | Book drawdown stop | Flatten the book and halt new entries once equity falls this far below its peak. Deliberately permanent for the rest of the run | "stop the book at 20%" | | Book take profit | The profit side twin: close everything and stop once the account is up past a target | "close the whole book at +30%" | | Max concurrent positions | Cap how many assets can be open at once | "at most 3 open positions" | | Total exposure cap | Ceiling on total open notional versus book equity | "cap exposure at 80% total" | | Per asset cap | Largest share of the book any one asset can hold | "max 20% per asset" | | Per sleeve loss stop | Force close any position that falls this far below its own entry, whatever the strategy's own exit says. It can reenter later | "stop each position that falls 12% below its entry" | | Sector exposure cap | Bound a whole group's share of the book, by naming the group's members | "cap memecoins (DOGEUSDT, SHIBUSDT) at 15%" | | Positions per sector | Bound how many *names* a group may hold at once | "max 2 positions per sector" | | Cash reserve | Never spend the book below this much cash | "always keep 20% in cash" | | Correlation cap | Skip a new entry whose recent returns move too closely with a position already open | "avoid holding two positions correlated above 0.8" | | Volatility target | Scale every new position so the book's realized volatility tracks a target | "target 15% annualized volatility" | | Sleeve drawdown halt | Retire an asset for the rest of the run once its own running P&L is this far below its peak | "stop trading any coin that is down 20%" | | Drawdown scaled sizing | Make every new position smaller while the book is in a drawdown, restoring full size on recovery | "halve position size while the book is in a 10% drawdown" | | Open risk budget | Bound the total *distance to stop* × size across every open position, ten positions each risking 1% is a 10% book risk | "never have more than 6% of the book at risk" | | Correlation clusters | Group everything that moves together and hold at most N names per cluster, the transitive answer where the correlation cap is the pairwise one | "at most one position per correlation cluster" | | Positions per symbol | On a book with several strategies on the same coin, bound how many of them may be open at once | "only one position per symbol", "BTC gets 3 positions max" | | Sectors by name | Tag your sectors in plain English once, then cap them by name | "BTC and ETH are majors, DOGE and SHIB are memecoins. Max 60% in majors, at most 2 memecoins" | | Minimum position size | Skip a fill that clears the venue minimum but is too small to be worth the commission | "skip any position under $1000" | | One sizing rule | Replace whatever the individual legs said with a single book wide rule | "risk 1% of the book per trade", "size every position at 10% of equity" | | Per position volatility target | Even out the sleeves so a wild coin and a calm one carry comparable risk | "target 20% volatility per position" | | Top N concentration cap | Bound what the biggest holdings add up to *together*. Four names at 24% each break this while breaking no single name limit | "cap the top 3 names at 60% of equity", "no more than 60% of the book in the top 3 holdings" | | Every sector, one number | Bound each sector without enumerating them, the way a mandate is written. Untagged sleeves are never capped by it | "never put more than half the book in one sector", "max 40% per sector" | | Liquidity floor | Skip an entry on any bar whose traded value was below a dollar figure, so the book never fills where the tape could not have | "require at least $10m of daily volume", "skip anything trading under $1m" | The **per sleeve loss stop** asks *is this open position underwater*, it closes anything trading below its own entry. The **sleeve drawdown halt** asks *has this sleeve given back its gains*, a coin that ran up 30% and round tripped the lot never triggers the first one, because no single position was ever deeply below entry. Use the loss stop as a safety net on a leg with no stop of its own; use the drawdown halt to stop feeding an asset that has stopped working. "Cap memecoins at 15%" and "max 2 positions per sector" are not the same instruction. One oversized name can consume a whole notional cap on its own, while two small ones fit comfortably inside it. Use the notional cap to bound risk, the count cap to force breadth. ### Timing and pacing | Control | Effect | Say | | --- | --- | --- | | New positions per period | However many signals fire, only this many new positions open per bar, day, week or month. The strongest get the slots | "open at most 1 new position per day" | | Daily / weekly loss limit | Stop opening for the rest of the period once the book is down this much from the period's opening equity, in percent or in dollars, then resume next period | "stop trading for the day after a 3% loss", "cap the daily loss at $500" | | Quit while ahead | The profit mirror: stop opening once the period has banked its target. Entry side: open positions keep managing themselves, so the banked number can still drift | "stop for the day after making $1000", "done for the day once we're up 2%" | | Reentry cooldown | After a sleeve closes, that asset waits this many of its own bars before it can be bought again | "no reentry for 5 bars" | | Minimum holding period | A signal exit cannot close a position before it has been held this many bars | "hold every position for at least 5 bars" | | Scheduled flatten | Close every open position on a calendar boundary, then carry on trading | "close everything on Friday", "no positions over the weekend" | | Trading calendar | Only open positions on these days or months | "only trade Monday to Thursday", "don't trade in December" | | Quarters | Only open in certain quarters | "only trade in Q4" | | Days of the month | Sit out particular dates, or the days around month end | "never trade on the 1st", "no trading around month end" | | Trading hours | Only open inside a UTC window, the intraday version of the calendar | "only trade between 08:00 and 16:00 UTC" | | One time blackouts | Sit out a named date range | "don't trade between 2024-12-20 and 2025-01-05" | | Book warm up | Let the book settle before it commits capital | "wait 50 bars before trading", "skip the first month" | | Total trade budget | A hard ceiling on trades for the whole run | "no more than 100 trades in total" | | Trades per period | The same ceiling, per day, week or month | "no more than 5 trades a week" | | Entry spacing | Require a gap between any two entries, so the book cannot go on all at once | "leave at least 3 bars between any two entries" | | Turnover cap | Bound annualized turnover as a share of equity, counting both sides of a round trip | "keep annual turnover under 200%" | | Breadth confirmation | Fund nothing unless this many sleeves signal on the *same* bar | "only enter when at least 2 assets signal together" | "At most 3 open positions" bounds how much the book holds. "One new position a day" bounds how fast it gets there, a book allowed three names can still be told to leg into them one per day. And the daily loss limit is the **recoverable** twin of the book drawdown stop: that one halts for the rest of the run, this one sits out the session and starts again tomorrow. ### Direction and regime | Control | Effect | Say | | --- | --- | --- | | Gross long / short caps | Bound each side of a long/short book separately | "max 60% long and max 40% short" | | Positions per direction | Bound how many *names* each side may hold at once | "at most 2 shorts at a time" | | Net exposure cap | Bound how far the book may lean either way | "keep net exposure under 20%", or "run it market neutral" | | Equity curve filter | While book equity is below the average of its own recent equity, stop opening, or go fully to cash, and resume when the curve recovers above it | "stop trading when the equity curve drops below its 50 day average" | | Market regime gate | Nothing in the book opens unless a condition on another symbol holds | "only trade when BTC is above its 200 day EMA" | A total exposure cap cannot express a market neutral book: it reads a 100% long book and a 50/50 long/short book as the same number. The net cap is judged across the whole bar, so a hedged pair is never refused for being "too long" on its first leg, only a lopsided bar loses its weakest name on the heavy side. It defers **signal** exits only, the indicator or comparison rule you wrote as your exit. A stop loss, take profit, trailing stop, liquidation and an explicit "exit after N bars" always fire, at any age. A minimum hold that swallowed a stop would be a risk, not a risk control. Closing out every Friday removes weekend gap risk and removes every trend that would have run straight through the flush. It is a real trade off, not a free safety measure, run the same book with and without it before keeping it. It damps long drawdowns, and it also guarantees you miss the first leg of every recovery, the curve has to climb back above its own average before the book will trade again. Judge it on the whole equity curve, not on max drawdown alone. The regime gate can also act rather than merely pause, and it speaks plain words: | Instruction | Effect | | --- | --- | | "go to cash when BTC drops below its 200 day MA" | A regime break CLOSES what the book holds, not just new entries; positions reopen on their own signals when the regime returns | | "go to cash in a bear market" | Read as BTC below its 200 day SMA, flattening, both halves flagged as inferred | | "avoid trading when BTC is dropping fast" | BTC's 7 day rate of change staying above −10%, flagged as inferred | | "only trade alts when BTC is stable" | BTC's NATR(14) below 5%, flagged as inferred | | "go to cash when @regime-score drops below 40" | Your own uploaded series decides risk on or risk off for the whole book | A daily regime filter on a 4h book reads the last **completed** daily bar, so there is no look ahead. During the warm up, before the regime series has enough history, the gate is closed: a filter that has not resolved yet is not permission to trade. ### Entry filters on each leg's own chart The regime gate above judges ONE symbol for the whole book. These ask the same question of EVERY chart separately, which is the shape most trader filters take: "only trade when ADX is above 25" means each leg's own ADX. | Control | Effect | Say | | --- | --- | --- | | Indicator filter | Entries only while an indicator condition holds on that chart | "only trade when ADX is above 25", "only enter when RSI is below 70" | | Custom filter | The same gate driven by your own uploaded indicator | "only trade when @trend-score is above 70" | | Price vs its own average | The per chart trend filter | "only go long when price is above the 200 day moving average" | | Trend alignment | Longs above the average, shorts below it: one instruction, two mirrored conditions | "only trade in the direction of the 200 EMA" | | Chop avoidance | ADX above 20, the conventional trend floor, flagged as inferred | "avoid choppy markets" | The signal still fires and lands in the skip ledger with its own reason, so the report shows how much the filter discarded. An open position always exits by its own rules, even when the filter has turned against it. And the filter fails CLOSED, a condition that cannot be evaluated blocks the entry rather than silently opening the book. ### Sessions and day boundaries | Control | Effect | Say | | --- | --- | --- | | Named sessions | An approximate UTC window (London 08:00-16:00, New York 13:00-21:00, Tokyo 00:00-08:00) flagged as inferred | "only trade the London session" | | Start every day flat | Flush the book on each UTC day's last bar; reenter on signals next morning | "start every day flat", "don't hold overnight" | | Avoid the open / close | The crypto day opens at 00:00 UTC | "avoid the first hour after the open", "no new positions in the last hour" | | First hours only | The mirror: only the day's opening burst | "only trade the first two hours" | ### Costs and execution A backtest with no fees and instant fills is a brochure, not a result. Both are stated once for the whole book. | Control | Effect | Say | | --- | --- | --- | | Fees and slippage | Apply the same cost assumption to every leg | "use 0.1% fees and 0.05% slippage across the book" | | Fill delay | Model the gap between a signal and a fill by entering N bars later | "assume a 1 bar delay on every entry" | | Next open fills | Fill every entry at the NEXT bar's open instead of the signal bar's close, removing a free fill at the very price the signal was computed from | "assume I get filled at the open of the following day" | | Short borrow | Charge the annual borrow rate as a per bar carry on every open short. Name the book's timeframe: an annual rate cannot be converted without it, and the run declines rather than guessing | "shorts cost 6% a year to borrow" | | Cash yield | Credit interest on idle cash, so a defensive book is not silently punished for being defensive | "earn 5% on the cash we are not using" | A signal computed at a bar's close cannot also be *filled* at that close: **next open fills** remove that free fill, and they will make almost any result worse. That is the point. The mirror case is **cash yield**: with idle cash earning exactly 0%, a book that correctly sat out a bad quarter still loses to a benchmark that compounded through it, and the comparison says nothing about the strategy. Turn both on before you compare two books. A frictionless run draws an honesty flag for a reason: at a few trades a month it barely matters, and at several a day it is the difference between a strategy and a fee generator. Run your real cost assumption before you believe a high turnover book. ### One stop and one target for every position The most used trader instruction of all, stated once for the whole book instead of repeated on every leg. An exit is added only where the leg does not already carry one of that type, the leg's own rule wins, and the result reports which legs took the book's exits and which kept their own. | Control | Effect | Say | | --- | --- | --- | | Book stop | A percentage stop on every position | "put a 2% stop on every position" | | Book target | A percentage target on every position | "take profit at 8% on every trade" | | Book trailing stop | Trail each position behind its own peak | "trail every position 3%" | | ATR stop | A volatility sized stop, a calm coin and a wild one get different distances from the same instruction | "use a 1.5 ATR stop on every position" | | ATR trail | The trailing version of the same idea | "trail every position with a 2 ATR stop" | | Indicator exit | Close each position when its own chart says so | "close every position when RSI is above 80" | | Time stop | Close a position that has run out of time rather than price. Lowest priority: a trade that worked has already left by its own exit | "time stop after 20 bars", "exit if the trade has not worked in 10 bars" | | Max holding time | The calendar phrasing of the time stop, the ceiling that pairs with the minimum hold floor | "only hold trades for a maximum of 5 days" | "Put a 2% stop **on every position**" is a book instruction; a bare "stop loss 2%" belongs to the leg that says it. That anchoring is what lets both live in one prompt without stealing from each other. An ATR exit on a series where ATR genuinely cannot be computed is refused and reported, never silently added, a stop the engine can never fire would leave the position unprotected while the config says otherwise. ### The risk unit Traders think in R: what a trade risks, and what it pays if it works. | Control | Effect | Say | | --- | --- | --- | | Reward to risk | Every leg with a percentage stop gets a target at stop × R | "risk 1% per trade with a 2 to 1 reward to risk", "target 2R on every trade" | | Fixed dollar risk | Size so a stop out costs a stated amount. Does NOT compound, that is the point of stating it in dollars | "size every position so it risks $500" | | Per trade ceiling | A cap, not a sizing rule: a leg asking for less keeps its size, one asking for more is trimmed | "never let one trade lose more than 1% of the account" | | Portfolio heat | The open risk budget, under the name traders use: distance to stop × size summed across open positions | "keep total heat under 6%" | | Risk in words | Risk below 1%, written the way people write it | "risk half a percent per trade" | R is a ratio, so it needs a percentage stop to multiply. A leg with an ATR or swing stop, or one that already states its own target, is listed in the result as skipped, "target 2R" that quietly did nothing on half the book is worse than an error. ### Scaling out | Control | Effect | Say | | --- | --- | --- | | Ladder | Take profit in pieces; whatever the rungs don't add up to keeps running | "sell a third at 5%, a third at 10%, and hold the rest" | | R denominated rung | A rung at a multiple of the leg's own stop | "scale out half at 2R and let the rest run" | | Partial then full | Half at the first level, everything left at the second | "partial exit at 5% and full exit at 10%" | Sizes are shares of the ORIGINAL position: "a third, then a third" really closes a third each time, with the remaining quantity arithmetic handled for you. Slices appear on the closed trade as partial exits, the same shape the single asset report already renders. ### Pyramiding discipline | Control | Effect | Say | | --- | --- | --- | | Never average down | Refuse any add while the position is at or below its blended entry | "add to winners only", "never average down" | | Pyramid cap | Total entries per position, the opener included | "pyramid up to 3 entries" | | …counted in adds | The same cap stated the way people actually say it. **Two adds is three entries**, because the opener counts | "add to a winner at most twice" | A refused add leaves the position exactly as it was and is counted in the result, a disciplined book shows what it declined rather than looking like the adds never fired. ### Protecting a profit | Control | Effect | Say | | --- | --- | --- | | Book profit lock | A ratchet: once the book is up past a trigger, defend a floor. The floor only ever moves up, so a give back cannot unwind a lock already earned | "once the book is up 20%, never let it go below breakeven" | | One number profit lock | The same ratchet with only the trigger stated, the floor is inferred at half the trigger, and the result says so | "lock in profits above 20%", "protect gains once up 15%" | | Give back fraction | The book trailing stop as a fraction of the peak | "give back no more than a third of the peak" | | Book trailing stop | Trail the whole book a fixed distance behind its own peak | "trail the book 10% off its peak" | | Stop management | Move every position's stop to breakeven, or start trailing it, once it is far enough in profit | "move every stop to breakeven after +5%" | ### Sizing that responds to something All of these are multipliers on the size you asked for, and they compose: with a ceiling, so several cannot conspire into a position nobody asked for. | Control | Effect | Say | | --- | --- | --- | | Size by recent record | Cut size after a losing streak, press after a winning one | "halve size after two losses", "double size after two winning trades" | | Ease into full size | Ramp from a starting exposure to full size over a period | "start at 25% exposure and scale to 100% over 3 months" | | Kelly fraction | Size from the book's own measured edge | "use half Kelly sizing" | | Market state rules | A multiplier gated on a condition, judged on each leg's own chart at entry | "halve size when ADX is below 20", "reduce size in high volatility" | | Custom conviction | The same rule driven by your own uploaded indicator | "double size when @conviction is above 80" | The Kelly size is computed from the book's own closed trades and stays inert below 20 of them. A bare "Kelly sizing" is read as **half** Kelly and the result says so, full Kelly is far too volatile to run, and a measured edge that turns out to be negative sizes to zero rather than short. ### Circuit breakers The count based cousins of the daily loss limit: that one counts percent lost, these count events. | Control | Effect | Say | | --- | --- | --- | | Losing streak halt | Stop opening for the rest of the run after a run of losing trades | "stop the book after 5 consecutive losing trades" | | Losing streak pause | Stand down for a number of bars, then come back | "pause for 5 bars after 2 losses" | | Done for the day | Stand down for the rest of the day, week or month | "stop trading for the rest of the day after 2 losses" | | Losing periods | Stop after a run of losing days, weeks or months | "stop trading after 3 losing days in a row", "halt after 3 losing weeks" | | Performance floor | Stop once the measured win rate or profit factor falls through a floor | "stop if the win rate drops below 40%" | | Rolling Sharpe floor | Stop opening while the *risk adjusted* record is under a floor, the drawdown blind twin of an equity curve filter. State no window and it is read over the last 60 equity points, and the run tells you it assumed that | "stop trading if the rolling sharpe drops below 0.5", "stop opening while our 90 day Sharpe is negative" | | Behind the benchmark | A RELATIVE drawdown stop: the book can be UP and still trip this, because what it measures is the gap to buy and hold | "benchmark against BTCUSDT buy and hold and stop trading if we underperform the benchmark by 15%" | "Stop trading" means take no new risk. Anything already open still manages itself by its own exits, because closing a book at whatever the market happens to be showing is a different instruction from the one you gave. A performance floor is also inert below 20 closed trades: halting a book on noise is worse than not halting it. ### Aiming a control at one coin Almost every control above can name a single asset instead of the whole book. The more specific statement always wins. ```text Cap BTCUSDT at 40% and ETHUSDT at 20%. Risk 2% on BTCUSDT and 1% on ETHUSDT. Stop BTCUSDT at 10% and cut ETHUSDT if it's down 8%. Hold ETHUSDT for at least 5 bars. Wait 10 bars before re-entering BTCUSDT. ``` Aiming a cap at an asset that is not in the portfolio is refused rather than quietly dropped, a cap that silently is not there reads exactly like a cap that is. ## Custom indicators at the book level Anywhere a built in indicator works in a book level control, a custom (`@` named) indicator you have uploaded works too. The series is loaded per leg through the same resolver a leg's own custom condition uses, so an unknown name fails the run loudly instead of gating on a column that is not there. | Surface | Say | | --- | --- | | Entry gate | "only trade when @trend-score is above 70" | | Position sizing | "double size when @conviction is above 80" | | Book regime, and go to cash on it | "go to cash when @regime-score drops below 40" | | Rotation ranking | "hold the top 2 by @score, rebalance weekly" | | Position exits | "close everything when @exit-score is above 80" | An entry **filter** that cannot be evaluated blocks the entry, a gate that is not there must not silently open the book. A sizing **rule** that cannot be evaluated leaves the size alone, a rule that is not there must not zero a position the strategy asked for. One is permission, the other is adjustment, and each fails in the direction that does less damage. ## Capital deployment and signal selection | Instruction | Effect | | --- | --- | | "only take the strongest signal each day" | Strength contention picks WHICH signal wins the capital; a one per day allowance decides HOW OFTEN | | "rank the signals and take the best two" | When five sleeves fire together, the two best ranked open and the rest are ledgered | | "trade with 50% of capital and keep the rest in reserve" | The cash reserve floor, stated from the deployed side | | "deploy capital gradually over the first month" | An exposure ramp from 0% to 100% of the stated size, flagged as inferred | | "withdraw profits monthly" | ALL of the gain out each month: flagged as inferred, because it turns off compounding and you should see that stated | ## What the book is measured against Every portfolio is compared to a **benchmark**, and by default that is an equal weight buy and hold of the book's own constituents. That is often not the comparison you meant, so you can say which one you want. | Benchmark | Say | | --- | --- | | Buy and hold one asset, including one the book does not trade | "compare it against BTC buy and hold" | | A stated mix, bought once and left to drift | "compare to a 60/40 BTC ETH portfolio" | | The same mix, systematically rebalanced | "compare to a 60/40 BTC ETH portfolio rebalanced monthly" | | Cash, a flat line at your starting capital | "compare against sitting in cash" | Whatever you pick becomes the denominator for the numbers that actually answer "was this worth it": outperformance, beta, correlation, annualized alpha, tracking error, information ratio, and up/down capture. Add "assume a 4% risk free rate" and the alpha is measured over that hurdle instead of zero. Beta against cash is not zero, it is undefined, a flat benchmark has no variance to regress against. The report leaves those fields empty rather than printing a number it cannot justify. Against cash, the outperformance figure is the whole story. "A 60/40 portfolio" normally means one bought 60/40 and left alone, so that is the default. Saying "rebalanced monthly" makes it systematically rebalanced, which harvests the mean reversion between its legs and is materially harder to beat in a choppy market. ## Deposits and withdrawals A book can also move money across the **account boundary** on a schedule: pay in every month, or take a share of the profits out. | Instruction | Effect | | --- | --- | | "add $1000 to the account every month" | Cash is added to the pool; the strategy then puts it to work on its own signals | | "withdraw 20% of profits every month" | A share of the gain above the starting capital is taken out | | "take out $2000 each month" | A fixed amount is taken out | This is **not** the same instruction as DCA. Dollar cost averaging deploys the pool the book already has, buying a fixed amount on a schedule. A deposit grows the pool and leaves the timing to your rules. A withdrawal only ever comes out of cash the book is actually holding, it never sells a position to fund one, and a profit share never touches the starting capital. Money you paid in raises the final equity without the strategy earning a cent, so a plain "total return %" on a contributed book is not a performance figure. The report carries a **contribution adjusted return** measured against the money actually put in, with withdrawals added back, that is the number to judge the book on. It is reported for **every** cash flow, however you phrased it. A book may state each cash flow **once**. "Withdraw $1,000 a month" is one instruction; saying it twice, once in prose and again as an explicit setting, used to make the book pay out twice, so an overlapping pair is now refused by name rather than applied silently. Deposits, fixed withdrawals and profit shares are each independent, so a book can state all three. A withdrawal and a profit share are each carried by their own setting at **every** cadence: monthly, quarterly or yearly, so the same instruction never lands on a different key depending on how often it repeats. A **deposit** is the one flow with a single home, since dollar cost averaging is a different instruction: it deploys the pool the book already has rather than growing it. ## Cross asset conditions One asset's entry can depend on another. Gate a trade on a second symbol and the splitter attaches it as a cross asset condition on the dependent asset. ```text When BTC RSI crosses above 65 on 1d, if ETH RSI is above 50, buy ETHUSDT. ``` A cross asset condition that gates an **entry** runs at execution time. Cross asset **exit** conditions are not live yet: a book that relies on one is refused before it runs, with the note "aux signal exit: pending parity". Rewrite the exit as a stop, a target, or a same asset condition. ## Reading the book report A portfolio report shows what a single backtest cannot. - **Book equity versus its benchmark.** The shared equity curve plotted against the benchmark, with a beating or trailing readout. The benchmark is an equal weight buy and hold of the same assets unless you asked for another one (see [What the book is measured against](#what-the-book-is-measured-against)), and it carries the relative numbers alongside it: outperformance, beta, correlation, alpha, tracking error, information ratio, and up/down capture. - **Per asset results.** Either full per asset panels (grade, equity, trades) or a **Per asset attribution** table (realized P&L, trades, win rate, exposure). Per asset return and Sharpe are drawn on an equal base display axis, a slice of the pool, not standalone capital. - **Skip ledger.** Every entry signal that did not fill, with its reason. Read it before concluding a strategy did nothing: a book that suddenly trades less is usually a control doing its job. | Reason | What happened | | --- | --- | | `NO_CAPITAL` | the signal fired but the shared pool had no cash left for it | | `BELOW_MIN_NOTIONAL` | the affordable size was under the exchange minimum | | `GUARD` | a per asset gate blocked it (session window, time filter, trading guard) | | `PRE_LISTING` / `STALE_BAR` | the asset had not listed yet, or had no bar closing on that tick | | `PER_ASSET_CAP` / `MAX_CONCURRENT` / `EXPOSURE_CAP` | a size or count cap was already reached | | `CORRELATION_LIMIT` | its returns were too correlated with a position already open | | `GROUP_CAP` / `GROUP_POSITION_CAP` | its sector was at its exposure cap, or already held its maximum number of names | | `TRADE_THROTTLE` | the book had already opened its allowance of new positions for the period | | `LOSS_LIMIT_PAUSE` | the book was paused for the rest of the period after hitting its loss limit | | `REENTRY_COOLDOWN` | the sleeve was still inside its post exit cooldown | | `LONG_EXPOSURE_CAP` / `SHORT_EXPOSURE_CAP` / `NET_EXPOSURE_CAP` | it would have pushed exposure past a directional cap | | `CASH_RESERVE` | funding it would have spent the book below its cash reserve floor | | `EQUITY_CURVE_FILTER` | the book's own equity was below its moving average, so it was standing down | | `REGIME_OFF` | the book wide regime filter was risk off on that bar | | `DIRECTION_MAX_POSITIONS` | the book was already holding its maximum number of positions on that side | | `SLEEVE_HALTED` | that asset was retired for the rest of the run by the sleeve drawdown halt | | `BOOK_TIME_FILTER` / `DATE_BLACKOUT` | the book does not open on that day, hour or month, or the date fell inside a blackout window | | `DIRECTION_BIAS` | the book is restricted to one side and that signal was on the other | | `WARMUP` | the book was still inside its settle period | | `TRADE_BUDGET` / `TRADE_PERIOD_BUDGET` | the trade allowance for the run, or for this period, was spent | | `ENTRY_SPACING` | it came too soon after the previous entry | | `TURNOVER_CAP` | annualized turnover was already at its ceiling | | `CONFIRMATION` | fewer sleeves signalled together than the book requires | | `BELOW_BOOK_MIN_NOTIONAL` | it would have been smaller than the book's own minimum position size | | `OPEN_RISK_CAP` | funding it would have pushed total open risk past the book's budget | | `GROUP_MAX_POSITIONS` | its sector/group was already holding its maximum number of positions | | `ASSET_TRADE_THROTTLE` | that sleeve had already opened its allowance of positions for the period | | `PER_ASSET_NOTIONAL_CAP` | the sleeve was already at its dollar cap for open notional | | `PERIOD_LOSS_LIMIT` | the book was stopped for the rest of the period after hitting its loss limit | | `LOSS_STREAK_PAUSE` | the loss streak breaker had paused new entries | | `DAILY_GIVEBACK_STOP` | the book had given back too much from the day's peak and stopped for the day | | `OUTSIDE_TRADING_DAYS` | the bar fell outside the weekdays the book may open on | | `OUTSIDE_TRADING_HOURS` | the bar fell outside the session window the book may open in | | `BLACKOUT_DATE` | the bar fell inside a date range the book sits out | | `DIRECTION_NOT_ALLOWED` | the book is restricted to the other side (long only or short only) | | `BELOW_MIN_VOLUME` | the bar traded less value than the book's liquidity floor | | `BELOW_MIN_TRADE_NOTIONAL` | the final size, after every cap, was under the book's minimum trade size | | `BELOW_MIN_SIGNAL_MARGIN` | the signal did not clear its own threshold by the required margin | | `BENCHMARK_LAGGING` | the asset had not outperformed the benchmark over the lookback | | `INSUFFICIENT_HISTORY` | the asset did not yet have the minimum bars of history behind it | | `ROLLING_SHARPE_FLOOR` | the book's rolling Sharpe was below its floor | | `TOP_N_CONCENTRATION` | the fill would have pushed the largest N positions past their combined cap | | `ASSET_RETIRED` | the sleeve had lost too much and stopped taking new signals | | `WIN_STREAK_BREAKER` | the book was standing down after a run of winning trades | | `ASSET_ATTEMPT_CAP` | that coin had already used up its allowance of entry attempts | | `OPEN_LOSS_GATE` | a position the book already held was too far underwater to take new risk | | `DRAWDOWN_PAUSE` | the book was too far below its high to open anything new, and had not recovered yet | | `BIG_LOSS_COOLOFF` | the book was cooling off after a single trade lost more than its limit | | `BAR_ENTRY_CAP` | the book had already opened its allowance of positions on that bar | | `SIGNAL_EXPIRED` | a signal the book could not fund waited too long and was dropped | | `SIGNAL_NOT_HELD` | the entry condition fired but had not held for long enough | | `SIGNAL_TOO_WEAK` | it cleared its own threshold by less than the other signals on that bar | | `BETA_CAP` | the book's beta to its benchmark was already at its ceiling | | `BELOW_MIN_PRICE` | the asset was trading below the minimum price the book will touch | | `VOLATILITY_STANDDOWN` | the book's own realised volatility was above its ceiling | | `CLUSTER_CAP` / `SYMBOL_POSITION_CAP` | its correlation cluster, or that symbol, was already full | | `LOSS_STREAK_BREAKER` / `LOSING_PERIOD_BREAKER` | the book was standing down after a run of losing trades, or of losing days | | `PERFORMANCE_FLOOR` | the book was halted because its win rate or profit factor fell through its floor | | `ENTRY_FILTER` | the book's entry filter was not satisfied on that leg's own chart | | `PROFIT_LIMIT_PAUSE` | the book had banked its profit target for the period and was done trading it | Some controls act on **exits** or on the account rather than on an entry signal, so they never appear in this ledger. They are counted separately, under the book's risk controls: a minimum hold deferring an exit, a scheduled flatten, a sleeve being retired, positions being sized down or up by a responsive or market state sizing rule, a position resized to a stated dollar risk or trimmed to its per trade ceiling, an R target added to a leg, a time stop firing, an add refused by the pyramiding discipline, the book sent to cash by a regime break, a delayed fill, the book's profit floor closing it out, and each deposit or withdrawal. Three entries in that ledger are **not** a control turning a signal away, and the report marks them as such: `ENTRY_FILTER_UNREADABLE` means the filter could not be *measured* on that asset (it did not fail the filter); `NO_NEXT_BAR` means the signal fired on the last bar of the data and a next open fill needs a bar after it; and `LIMIT_NOT_FILLED` means the bar never traded through your limit, an execution miss rather than a rule. No fill is ever invented to hide one. - **Run warnings.** What the engine decided, said out loud. Three of them mean the run's numbers are **not comparable with a default run** and are styled to say so: `BOOK_COSTS_APPLIED` (one cost model replaced every leg's own), `FILL_TIMING_NEXT_OPEN` (entries filled at the next open, not the signal close) and `COMPOUNDING_OFF` (every position sized off the starting capital). `AVERAGED_DOWN` is styled as a warning of its own, it is the only family that adds risk to a losing position. - **Money that moved outside trading.** Withdrawals, profit shares and interest on idle cash all change what final equity means, so the header names them beside it. A *shortfall* count means a scheduled payout could not be funded in full. - **Retired sleeves.** If the sleeve retirement rule fired, the report names which asset the book gave up on and what it cost, usually the most actionable line in the whole thing. - **Book grade.** One letter grade for the whole book, from the same grader as a single backtest, alongside book level Monte Carlo and walk forward robustness. You can **share** a book as a read only `/sp/` page, and export the merged trades as CSV or the full result as JSON. ## Known limits These are behaviours that are *correct* but surprising, the ones most likely to make a run look broken when it is doing exactly what it was told. - **Spacing between entries is measured across bars, not within one.** "Leave 3 bars between any two entries" stops the book opening on bar 3 and again on bar 4. It does **not** stop three sleeves that all signal on the *same* bar from all opening at once, zero bars have passed between them. To bound a single bar's burst, say "at most one new position per bar"; the two rules compose. - **A total trade budget can finish slightly over.** "No more than 100 trades" stops the book *opening* once 100 have booked, but positions already open still run to their own exits and book as they go. The overshoot is at most the number of positions open when the budget was reached; cap concurrent positions (or new positions per bar) to bound it. - **"Require the signal to hold for N bars" cannot be met by a crossover.** A crossover is true on exactly one bar by construction, so asking it to hold for two blocks every entry and the book takes no trades. That is the rule being unsatisfiable, not an absence of edge, the run says so in its warnings. Use a threshold style entry ("RSI is below 30") for a signal that can persist. - **A stand down phrasing names the side you sit out.** "Stop trading when the equity curve drops below its 50 day average" means *trade while above it*. Both readings are supported, "only trade while equity is below its average" is a valid, if unusual, instruction, so the wording decides, and the report echoes the side the book actually traded on. - **A rotation that holds cash is the momentum floor working.** "Only while their momentum is positive" is an instruction to sit out, so a flat stretch with no trades in a falling market is the control doing its job, not a rotation that failed to rank. The run distinguishes the two: one warning says nothing cleared the floor, a different one says nothing could be ranked at all. - **A minimum number of holdings cannot be honoured.** "Always hold at least 3 positions" is declined out loud. A signal driven book can decline exposure but never manufacture it: there is no entry to take when nothing has signalled. State it as an allocation instead ("equal weight the book, rebalance monthly"), which holds targets rather than waiting for signals. - **A liquidity floor filters bars; it does not pick names.** "Require $10m of daily volume" skips entries on thin bars for the assets you named. Choosing *which* assets to trade by liquidity or market cap is refused: it needs a point in time constituent list, and ranking today's names over history would put survivorship bias into every bar. - **Spot crypto only** in v1; equities are rejected. A book holds up to **20 assets**. - **Mixed timeframes are allowed.** The book merges assets on absolute close time and reports the equity curve on the coarsest timeframe in the roster. - Per asset return and Sharpe use an **equal base display axis**, not standalone capital. Win rate, profit factor and trade counts are each asset's real numbers. - If the book hits the equity floor it **halts early** and flags ruin. - Futures books are not supported yet. - Starting capital stated in the prompt ("a $250k book") is carried into the run. If you drive the API or the MCP tools yourself, pass the `settings` object `parse_portfolio` returns straight to `run_portfolio`, it holds every book level control the prompt stated, and a book run without it is a book without your risk limits. Next: [Build a portfolio](/docs/guides/build-a-portfolio) step by step, or the [MCP tools](/docs/api/mcp) to run one programmatically. --- ## Analysis & export Source: https://docs.texttoquant.com/reference/analysis Summary: Regime context, probability scans, Monte Carlo, and exporting a run. Beyond the summary metrics, each run carries several deeper analyses that explain *why* it performed the way it did and *how much* to trust it. This page is the reference for those panels, plus how to take the run with you. Which panels you see depends on your plan: Monte Carlo and CSV export from Pro, context and the robustness suite from Power, and the probability scan on Enterprise. ## Context V3 Context analysis is on Power and above. Every trade is labelled by the market regime it happened in, so you can see where the edge lives, and filter to rerun on just one slice. | Dimension | Labels | | --- | --- | | Trend | Bull / bear / chop | | Volatility | Low → high | | Structure | Break / pullback | | Macro | Inflation tertile | Filter to a bucket and rerun to see slice only performance. A strategy that only works in bull trend, low volatility conditions is a very different bet from one that works everywhere. Those four are the defaults. Context can reanalyse across *any* built in indicator (RSI, ADX, …) or one of your custom indicators as the dimension, and by entry position, with a minimum sample threshold per bucket so a thin slice can't mislead you. ## Probability scan Enterprise only. The base rate for your *entry signal*. Your backtest holds one position at a time, so every trigger that fires while a trade is open is skipped, the trade count is a floor, not how often the setup actually happens. The scan rewalks **every** trigger and asks what happened next. | Metric | Meaning | | --- | --- | | Hit rate | Share of **resolved** signals that reached the target | | Break even | The hit rate your realised payoff actually needs | | Signal edge | Hit rate minus break even, the part that is really an edge | | Expectancy | Mean return per signal, gross of fees | | Signals | Times the trigger fired, and how many became trades | | Avg win / loss | With average MFE and MAE beneath each | 27% is excellent at a 3:1 payoff and fatal at 1:1. Always read the hit rate against the **break even rate** beside it. The verdict is decided by the 95% confidence interval, not the raw number, and stays **inconclusive** below 30 resolved signals however good the rate looks. **Pending signals are excluded from every rate.** A signal the scan could not resolve is unknown, not unsuccessful, counting it as a loss would understate exactly the strategies whose trades run longest. ### Traded vs skipped The reason the scan exists. Every trigger is matched to a real trade, so you can see which ones your backtest could not take and whether they resolved any differently. The two groups are compared by **confidence interval overlap, not by their printed rates**, a 30 signal group and a 7 signal group will nearly always overlap, and when they do the honest answer is that the sample cannot say whether the position filter helped or hurt. The full list lives in the trade table's **SIGNALS** view, with a TRADED / SKIPPED column. Those skipped triggers appear nowhere else. ### How outcomes resolve With a profit target set, each signal is walked forward, up to 500 bars, and resolved on whichever comes first: **stop, target, trailing stop, or time exit**. When a bar's range spans both stop and target, intrabar order is unknowable, so it resolves as the stop. Without a profit target it falls back to a fixed 20 bar close to close return against a ±0.1% flat band. Indicator exits, stop plans and new high/low exits are **not** simulated. When your strategy carries one, the panel names it. And **Model vs reality** measures the gap directly: on the triggers your backtest did trade, how often the modelled outcome matched the real trade. High agreement means you can trust the rest; low agreement means unmodelled exits are doing the work. Everything here is gross of fees and slippage, and position independent, it has no size, so it has no P&L. Overlapping triggers can also count the same move more than once. Read it alongside the backtest, never instead of it. Heavy (chunked) backtests stream the series in slices and cannot run the scan; the panel says so rather than showing an empty deck. ## Monte Carlo Pro and above. Your equity curve is one path through history. Monte Carlo resamples the trade order thousands of times to show the *range* of outcomes you could have had, and how deep a drawdown to expect. Risk bands appear after 5+ trades. | Metric | Meaning | | --- | --- | | P(ruin) | Severe loss tail | | 95% band | Outcome range | | Percentiles | Best / worst paths | ## Export Take the run with you as data files or a share card. CSV export is on Pro and above; signals come from the probability scan, so that file is Enterprise. | Format | Contents | | --- | --- | | CSV (trades) | Entries, exits, P&L, R, fees | | CSV (equity) | Equity curve over time | | CSV (signals) | Probability scan signals | | PNG card | Metrics + logic snapshot | ## Deeper validation Context, probability and Monte Carlo all read the run you already have. To pressure test the edge *itself*, the [robustness suite](/docs/reference/robustness) (Power and above) reruns the strategy on new data and new parameters: | Check | Question | | --- | --- | | Last 30% vs first 70% | Did the edge persist through the last 30% of the window? Nothing is fitted: this is a chronological split of the run you specified. | | Window consistency | Is the edge present across sequential windows of the same run? | | Parameter sweep / grid | Is the result a stable plateau or a lone spike? | See [robustness](/docs/reference/robustness) for the full suite and the overfit verdict. Related: [Metrics](/docs/reference/metrics), [Robustness](/docs/reference/robustness). --- ## Robustness Source: https://docs.texttoquant.com/reference/robustness Summary: The overfitting verdict and the checks behind it. A single good backtest proves very little. The robustness panel gives one honest verdict, *is this edge likely to survive?*, backed by several independent checks that each attack the result from a different angle. The robustness panel, sweeps, walk forward and the overfit verdict are on Power and above; Monte Carlo is on Pro and above. ## The checks | Check | What it asks | | --- | --- | | OOS split | Does it hold on data it was never fit on? (in sample vs out of sample) | | Walk forward | Is performance consistent across rolling windows? | | Regime | Does it work in bull, bear and chop, or only one? | | Cross market | Does the edge transfer to other assets? | | Sample size | Are there enough trades to trust the numbers at all? | Instant legs read the current backtest and appear immediately; heavier legs (cross market, walk forward optimization) run on demand. Cross market tests four peer markets by default, picked from your strategy's own asset class. In the terminal's Robustness section you can name them instead: switch **Cross market** to *Choose markets* and add up to four symbols. The run is billed the same flat 5 backtests, the timeframe leg is unchanged, and your own market is refused as a peer (it would only agree with itself). Agents can do the same over MCP with `start_analysis` kind `multi_asset` and `params.assets`. Read a chosen run more carefully than an automatic one: a market you picked that *breaks* the strategy is still hard evidence of fragility, but markets that hold up are a set you selected, so they cannot show that the edge generalizes. ## Parameter search checks These run real backtests across parameter variations to test whether the edge is a stable plateau or a lone spike. They're billed (see [plans & credits](/docs/concepts/plans-and-credits)) and run on demand. | Check | What it does | | --- | --- | | Parameter sweep | Reruns across values of one knob, is the result robust across the range, or a single peak? | | Parameter grid | A 5×5 sweep of two knobs (25 runs); the CSCV split of a grid is what produces the PBO score | | Joint sweep | Varies up to three parameters together to test region stability, not just one axis at a time | | Genetic search | Evolves parameter sets against real backtests to find, and stress, the best region | | Strategy variants | Automatically runs long only / short only / risk scaled versions and ranks them | | Walk forward efficiency | Out of sample return ÷ in sample optimum; a low WFE flags a fragile, overtuned fit | ## Overfitting aware statistics A raw Sharpe is inflated by how many configurations you tried. Once a parameter search records the trials, these statistics appear on the metric cards, an honest read no other natural language tool ships. PSR, DSR, Haircut and Min. length come from any parameter sweep; **PBO needs the CSCV split, so it appears only after a grid or joint sweep**. | Metric | Corrects for | How to read it | | --- | --- | --- | | Probabilistic Sharpe (PSR) | Short samples & fat tails | Higher = more confident the Sharpe beats 0 | | Deflated Sharpe (DSR) | How many configs you tried | ≥95% = survives the search; a big drop vs PSR = search luck | | Overfit Probability (PBO) | In sample best failing out of sample (grid / joint sweep only) | Low is good; ≥50% = likely overfit | | Haircut Sharpe | Bonferroni correction for T trials | The Sharpe you can still claim after the search | | Min. backtest length | Sample too short for the search | Warns when history can't support that many trials | Over the [MCP server](/docs/api/mcp), `get_overfit_verdict` (or `GET /v1/backtests/:id/overfit`) returns this same read: Deflated Sharpe, PBO and the `holds_up` / `likely_overfit` / `insufficient_evidence` call. A single backtest with no parameter search honestly returns `insufficient_evidence`: run a sweep or grid first. Your query compiles deterministically to a fixed strategy spec, then a fixed engine scores it. The AI never sees, ranks, or tunes the numbers. Same query ⇒ same spec fingerprint ⇒ same test. Signals use only closed bar data (no look ahead). The results header shows a Reproducible chip and, when a hold out split exists, an always on out of sample verdict. See how these feed the letter grade in the [Metrics reference](/docs/reference/metrics). --- ## Execution model Source: https://docs.texttoquant.com/concepts/execution-model Summary: Fills at the signal bar's close, conservative intrabar ordering, and why look ahead is impossible. A backtest is only as trustworthy as the assumptions inside it. Every strategy, typed in plain English or assembled in the visual builder, compiles to the *same* engine, and where that engine has to make a judgment call at bar resolution, it makes the **conservative** one: ambiguous situations resolve against the trade, not in its favour. The goal is that a good result survives scrutiny, not that results look good. ## Signals, entries and exits - **Signals are evaluated on completed bars** (bar close). The engine never acts on a bar that is still forming. - **Entries are edge triggered:** a condition must become true *having been false on the previous bar* to fire. "RSI above 50" fires once when it crosses, not on every bar the condition merely stays true. - **Entry fills occur at the signal bar's close**, with configurable slippage applied against the trade direction. - **Intrabar exit ordering is conservative:** stop losses are checked before profit targets on the same bar. A bar that touches both your stop and your target books the loss. - **Fills are gap aware:** if a bar opens beyond a stop or target, the fill uses the open price, worse for a gapped stop, better for a gapped target, never the level the market skipped past. ## No look ahead The single most important guarantee: a backtest can never use information it wouldn't have had live. - **Multi timeframe and cross asset conditions read only the previous *completed* higher timeframe bar.** A 4 hour strategy consulting the daily trend sees yesterday's finished daily bar, never today's still forming one. - **Monthly bars use true calendar closes**, not fixed width approximations. - **Indicators are seeded with warm up data** before your requested start date, so a 200 period moving average is already correct on bar 1 of your window instead of spending the first months converging. The intrabar price path is unknown at bar resolution. When one bar touches both a stop and a target, the engine resolves the conflict conservatively (the stop) rather than by tick data. Backtests are hypothetical and benefit from hindsight; past performance does not guarantee future results. Related: [costs & fees](/docs/concepts/costs), [overfitting](/docs/concepts/overfitting), [robustness reference](/docs/reference/robustness). --- ## Costs & fees Source: https://docs.texttoquant.com/concepts/costs Summary: How commissions, slippage and position sizing are modelled. Real trading costs money, and a backtest that ignores that is lying to you. The engine charges fees on every fill and applies slippage against the trade direction, so a simulated fill is never better than a live order would have got. ## Fees & slippage - **Exchange style percentage fees** are charged on every fill: entries, exits, and partial fills alike. - **Slippage is configurable** and always applied against the trade direction: entries fill slightly worse than the signal price, never better. ### Say the costs in the sentence You don't need a settings panel to charge costs, state them in the query and the parser routes them for you: - **One flat rate:** *"… with a 0.1% trading fee"*, *"… with commission of 0.1%"*, or *"… with 10 bps fees"*, all charge the same single rate on every fill (bps convert automatically). - **Maker and taker rates:** *"… with maker fees of 0.05% and taker fees of 0.1%"*: two rates, charged per fill by side. - **Both together:** *"… with 0.1% fees and 0.05% slippage"* sets the neighbouring knob in the same breath. - **Explicitly free:** *"… with no fees"* records a real zero rather than an unstated default. A **single sided rate** (*"0.05% taker fees"* with no maker rate given) is treated as one flat rate, the engine never invents the missing side. And a maker/taker schedule needs **both legs**: a half stated or out of bounds schedule is refused with a clarification instead of being guessed, because guessing would silently make the run cheaper than you asked for. ### Which fills pay which rate With a maker/taker schedule set, bar close entries and every forced exit (stop, blow up, end of data) pay the **taker** leg, they cross the book. Resting limit fills pay the **maker** leg. A run that charges fees through a schedule counts as a costed run everywhere, including the grade: it will not be flagged as "graded without transaction costs". ## Execution realism Three optional models make fills more conservative. All are **off unless set**, an untouched run is unchanged, and every run is stamped with the fill model version it executed under, so results stay comparable across upgrades. - **Bid/ask spread** (`spreadBps`): a half spread charged on taker fills and **waived on maker fills**, crossing the book costs the spread; resting in it doesn't. - **Volume capped fills** (`maxBarVolumePct`): a fill may not exceed the set share of the bar's volume. Trades that get sized down say so (`volume_cap` vs `affordability`), so a smaller position is always attributable. - **Market impact** (`marketImpactK`): charges more the larger a share of the bar's volume your order takes. **Impact requires the volume cap**, without a fill cap the share is unbounded and so is the number, so the engine refuses impact without cap with a named warning rather than producing one. On perpetual futures, funding is available as a *queryable signal* (`funding rate`, `aggregated funding rate`) you can trade on, but it is **not** charged against P&L as a holding cost. If a strategy holds perps for long stretches, budget for funding yourself. ## Position sizing & liquidation How much you commit per trade shapes the equity curve more than almost anything else. - **Sizing modes:** risk based (risk a fixed % of equity per trade against your stop distance), % of equity, and fixed sizing, all with an **affordability cap**, so a position can never be larger than the account can actually pay for. - **Leveraged futures include a liquidation model**, and spot shorts carry a margin style liquidation backstop. - **Equity can never go below zero.** A blow up terminates the run rather than letting the simulation trade with money that no longer exists. Fees and slippage are inputs, not fixed constants. Model the fee tier and typical slippage of the exchange you'd actually trade on. A strategy that only works at zero cost isn't a strategy. See how sizing is expressed in a query under [entry & exit logic](/docs/reference/entry-exit), and how it's scored in the [Metrics reference](/docs/reference/metrics). --- ## Plans & credits Source: https://docs.texttoquant.com/concepts/plans-and-credits Summary: What a credit is, which actions spend one, and where to watch your usage. Reading and parsing are free; running a simulation costs a credit. This page is the honest accounting of what bills, what doesn't, and how the robustness suite multiplies cost so you can budget before you batch. ## What a credit is A **credit is one backtest.** Your plan grants a pool of them per billing period; you can see the pool at any time under [Account → Usage](/account/usage) or via `GET /v1/usage` (limit, used, remaining, period end). ## What costs credits | Action | Cost | | --- | --- | | Run a backtest | 1 | | Parameter sweep | 1 per backtest in the sweep | | Parameter grid (5×5) | 25 | | Walk forward, joint sweep, genetic search, strategy variants | a handful each (typically 5) | | Run a portfolio | 1 per asset in the book | | Portfolio sweep | values × assets (K × N) | ## What's free - **Parsing** a query, **reading** results and history, **sharing** a run, **exporting** to CSV or Pine, and running **screener** scans don't spend backtest credits. - **Monte Carlo, out of sample split and regime context** read the run you already paid for, no extra charge. - LLM parsing and Pine compilation are gated by plan *feature*, not metered per call. ## Plans Tiers, quotas and history/timeframe limits live on the [pricing page](/pricing). Higher tiers raise your monthly allowance and history depth, widen the timeframes and markets you may test, and unlock API access, custom Pine indicators, Pine export and the deeper robustness work. A free account gets a **one time** allowance of backtests for the life of the account, not a monthly one. It is enough to judge the product on your own strategy, not to run a research program. Scan alerts and the MCP connector are on every plan, free included; what a higher tier buys there is a larger number of active alerts, and a rolled up digest at the top. ## Rate limits On top of your credit pool, the API is rate limited to **240 requests/minute per IP** and a per account ceiling set by your plan: **60/minute on Pro, 120 on Power, 240 on Quant and Enterprise**. Over a limit you get a `429`. See [authentication](/docs/api/authentication) for the headers. The robustness suite runs many real backtests, a grid is 25, a sweep is one per value. Check `GET /v1/usage` before a batch, and confirm the cost before a large sweep you didn't explicitly ask for. Retries are safe: send an `Idempotency-Key` and a replay returns the original run with **no second charge**. Related: [robustness](/docs/reference/robustness), [authentication](/docs/api/authentication), [pricing](/pricing). --- ## Overfitting Source: https://docs.texttoquant.com/concepts/overfitting Summary: Why a good backtest is not the same as a good strategy. Overfitting (curve fitting) is tuning a strategy so tightly to past data that it captures *noise* instead of a real edge. An overfit strategy looks brilliant in the backtest and falls apart live. The more parameter combinations you try, the easier it is to find one that fit the past by pure luck. This is the central risk in all backtesting, so the platform is built to catch it. ## Why it happens Every extra parameter, every "let me just try 20 and 30 and 40", multiplies the number of ways a result can look good by accident. Keep the best of 100 variations and its headline Sharpe is inflated by the *search itself*, not by any edge that will repeat. ## The checks The report ships several independent checks, each attacking the result from a different angle: - **Seeded Monte Carlo resampling** with permutation testing: reshuffled trade orders and permuted returns show how much of the result is path luck. - **Out of sample splits**: performance on data the strategy was not tuned on. - **Walk forward optimization**: parameters refit on rolling in sample windows, judged only on the forward window that follows. - **Parameter sensitivity sweeps**: whether the result survives nearby parameter values or lives on an isolated spike. - **Multi asset robustness**: the same logic run on other markets. And the overfitting aware statistics, Deflated Sharpe, Overfit Probability (PBO), Haircut Sharpe, discount the headline numbers for how many configurations you tried. See the full table in the [Robustness reference](/docs/reference/robustness). Fit on one slice, judge on another you never touched. If the edge only exists on the data you tuned it on, it isn't an edge. It's a memory of the past. A strategy that passed every check on this page can still lose money live. Backtests are hypothetical, benefit from hindsight, and exclude live latency and venue specific costs. Past performance does not guarantee future results. Next: run the [validation workflow](/docs/guides/validate-an-idea) on one of your own strategies. --- ## Authentication Source: https://docs.texttoquant.com/api/authentication Summary: API keys, scopes and the request envelope. TextToQuant exposes a small REST API you can call from any language, plus an [MCP server](/docs/api/mcp) for AI agents. The REST API and the **downloadable** MCP server both authenticate with the **same API key** and share your billing and plan limits. The **hosted** MCP endpoint is different: it uses OAuth (scopes `mcp:read` / `mcp:run` / `mcp:write`), not an API key. See the [MCP page](/docs/api/mcp) for that flow. ## API keys Create a key under [Account → API keys](/account/api). It's shown once, so store it safely. You can have up to **5 active keys**, and revoking one cuts off access instantly. Send the key on **every** request, as either header: ```http Authorization: Bearer ttq_your_key # or x-api-key: ttq_your_key ``` Programmatic access requires a plan with API access. Keys are shown once at creation. Store them securely. ## Key scopes The research surface (parse, backtest, indicators, screener) needs **no scope**. Every key reaches it, and every key issued before scopes existed still does. The [trading surface](/docs/api/trading) is different, because it reads and acts on a live account. Two opt in grants gate it, and both default to **off**: | Scope | Grants | | --- | --- | | `trading:read` | Positions, balances, orders, fills, closed trades, P&L, risk and runtime state | | `trading:write` | The risk reducing controls: pause, cancel, close, set stop/take profit, kill switch | Choose them when you create a key. An existing key **never** gains a scope automatically: a key you pasted into a script years ago cannot suddenly read your book because we shipped a feature. `GET /v1/me` reports what a key actually has, including whether the plan clears the second gate: ```json { "success": true, "data": { "userId": "…", "scopes": ["trading:read"], "plan": { "tier": "pro", "liveTrading": true }, "capabilities": { "tradingRead": true, "tradingWrite": false } } } ``` It means the key is valid and simply was not granted that capability. Reissuing it will not help; granting the scope will. The response names `requiredScope` and `grantedScopes`. ## The response envelope Every response, success or failure, shares one shape. A `2xx` reply is the mirror of an error: ```json // success { "success": true, "data": { /* … */ } } // list endpoints also add pagination { "success": true, "data": { "results": [ /* … */ ] }, "pagination": { "limit": 10, "offset": 0, "total": 1, "hasMore": false } } // error { "success": false, "code": "invalid_api_key", "error": "Invalid or revoked API key. Create a new one at Account → API keys." } ``` The `code` is stable and safe to switch on; the human readable `error` text may be reworded. Error responses also include a `requestId`. Quote it when contacting support. ## Rate limits **240 requests/minute per IP**, and a per account ceiling set by your plan (**60/minute on Pro, 120 on Power, 240 on Quant and Enterprise**), on top of your plan quotas. Every response carries standard `RateLimit` headers (limit, remaining, reset). Pace yourself with them. Over a limit you get a `429` with code `rate_limited`. Trading **writes** carry an extra ceiling of **30 live account actions per minute per user**: they reach a venue, so they are budgeted separately from ordinary reads. ## Safe retries (idempotency) Send an `Idempotency-Key` header (or `idempotency_key` in the body) on `POST /v1/backtests`. Replaying the same key with the same body within 24h returns the original run, **no second credit**. The same key with a *different* body returns `409 idempotency_key_reuse`. ## Error reference | HTTP | code | What it means | | --- | --- | --- | | 401 | `missing_api_key` | No key on the request: add the header | | 401 | `invalid_api_key` | Key is wrong, revoked, or malformed | | 403 | `feature_not_available` | Your plan doesn't include this feature (`requiredFeature` says which) | | 403 | `insufficient_scope` | Trading endpoint, key lacks `trading:read`/`trading:write`: grant it on the key | | 403 | `upgrade_required` | Trading endpoint, plan has no live trading | | 503 | `plan_unavailable` | Trading endpoint, the plan couldn't be resolved: fails closed, retryable | | 403 | `plan_history_limit` / `plan_timeframe_limit` | Date range or timeframe beyond your plan | | 400 | `invalid_query` / `validation_error` | Bad input: the message names what failed. Nothing billed | | 422 | `clarification_needed` | Parse hit ambiguous terms: `data.clarificationsNeeded` lists them | | 422 | `missing_parameters` | Parse missing a required field: `details.missingFields` + suggestions | | 422 | `invalid_strategy` | Parsed strategy isn't executable: `details.structuralErrors` say why | | 422 | `cannot_execute` | Pre run validation failed: the message lists failing checks | | 409 | `idempotency_key_reuse` | Same key, different body: use a fresh key | | 413 | `WORKLOAD_TOO_LARGE` | Backtest exceeds the bar count ceiling | | 429 | `rate_limited` | Over the request rate: pause and retry | | 429 | `OVERAGE_*` | Billing state gate (pending / overdue / cap reached) | | 404 | `not_found` | Unknown endpoint, or an id you don't own | | 503 | `QUEUE_BACKPRESSURE` / `queue_unavailable` | Queue busy or down: retryable, credit refunded | | 500 | `execution_failed` | The run failed: nothing saved, metered credit refunded | | 500 | `parse_failed` | Parser couldn't read the query: `details.suggestions` hint a fix | | 500 | `internal_error` | Unexpected server error: safe to retry | Usually not. Backtests finish server side even if the HTTP connection drops, and the credit was already counted. Wait a moment, then call `GET /v1/backtests`. The newest row is your run. Next: the [endpoint reference](/docs/api/endpoints). --- ## Backtesting API (REST endpoints) Source: https://docs.texttoquant.com/api/endpoints Summary: The backtesting API: run backtests and read results over HTTP. Base URL: `https://www.texttoquant.com/api`. All endpoints require [authentication](/docs/api/authentication) and return the standard `{ success, data }` envelope. The whole surface is described in OpenAPI 3.1 at `GET /v1/openapi.json` (public). Generate typed clients straight from it. ## Endpoints Some endpoints need a plan above the one an API key starts on (API keys are on Pro and above). The tables mark them: *Power+* means Power, Quant and Enterprise; *Enterprise* means Enterprise only. A call your plan does not cover is refused before anything is billed. **Account & usage** | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/me` | Verify a key and see what it can do: owner, scopes, plan, and whether `/v1/trading` is reachable | | `GET` | `/v1/usage` | Plan tier and backtest credits: limit, used, remaining, period end | | `GET` | `/v1/me/notification-channels` | Which alert channels will deliver: Telegram linked? email? | **Parse & run** | Method | Path | Description | | --- | --- | --- | | `POST` | `/v1/parse` | Plain English → structured query. Send `{ "query": "…" }` (+ optional `customIndicatorHints`); get a `parsedQuery`. *Plan gated (LLM)* | | `POST` | `/v1/backtests` | Run a backtest from a `parsedQuery`. Supports `Idempotency-Key` and `savedIndicatorMapping`. *Bills 1* | | `GET` | `/v1/backtests` | Your run history, newest first, filterable & sortable (see below) | | `GET` | `/v1/backtests/:id` | One run in full: summary metrics, grade, edit lineage, exact parameters | | `GET` | `/v1/backtests/:id/status` | Cheap progress poll: `completed`, `running` (stage, percent), `failed`, `cancelled`. Use after a `202` | | `POST` | `/v1/backtests/:id/cancel` | Cancel a run that is still queued and refund its credit. A run already computing is not stopped (returns `running`). Never refunds a completed run | | `POST` | `/v1/backtests/:id/sweep` | Robustness suite (async → poll `GET …/:jobId`): `/sweep`, `/grid`, `/walk-forward`, `/joint-sweep`, `/multi-asset`, variants and screens. *Bills*, *Power+* | | `GET` | `/v1/backtests/:id/context` | Why trades won/lost by regime: `/context/trades`, `/context/reanalyse`, `POST …/context-run`, `POST …/context/bucket-trades`. *Power+* | | `GET` | `/v1/backtests/:id/overfit` | Overfitting verdict: Deflated Sharpe + PBO, deflated by the configs tried on the strategy session. *Power+* | | `POST` | `/v1/condition-insights` | Forward outcome base rates for one entry condition, a filter probe, not a backtest. *Power+* | | `GET` | `/v1/gallery` | Showcase strategies to start from, ranked by `sharpe`, `return`, `dsr`. Each card carries `author`: `{handle, kind}` (where kind is `username`, `email` or `anonymous`) or `null` | | `POST` | `/v1/portfolio/*` | **Portfolio endpoints (the rows below).** With an API key they need the Enterprise portfolio builder. Power and Quant run portfolios only through a connected AI agent (MCP), not with a key | | `POST` | `/v1/portfolio/sweep` | Rerun a whole book across 2-5 values of one knob. *Bills K×N*, `Idempotency-Key` supported, poll `GET …/:jobId`, `POST …/:jobId/cancel` refunds unrun books | | `GET` | `/v1/portfolio/runs/:id/analysis` | A completed book's analysis: metrics, grade, attribution, benchmark, Monte Carlo, walk forward, out of sample | | `POST` | `/v1/portfolio/parse` | Split a multi asset prompt into per asset strategies | | `POST` | `/v1/portfolio/execute` | Run a shared capital portfolio (async → poll `GET …/:jobId`). *Bills 1 per asset* | | `GET` | `/v1/portfolio/execute/:jobId` | Poll an async portfolio run | | `POST` | `/v1/portfolio/runs/:id/share` | Mint a portfolio run's public share link (`DELETE` the same path revokes it) | **Data & results** | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/runs/:runId/equity` | Raw datasets: `equity`, `trades`, `candles`, `monte-carlo`, markers, condition logs (paginated) | | `GET` | `/v1/runs/:runId/indicators/:name` | Plotted values of one indicator on a run | | `POST` | `/v1/backtests/:id/share` | Mint the public share link (`DELETE` the same path revokes it) | | `GET` | `/v1/backtests/:id/chart` | Fresh chart payload (series + markers) for a saved run | | `GET` | `/v1/backtests/:id/sweep/options` | The sweepable knobs available for a run | | `GET` | `/v1/strategies/:id/iterations` | Edit/version history of a strategy session | | `PATCH` | `/v1/backtests/:id/tags` | Replace a run's organization tags | | `PATCH` | `/v1/strategies/iterations/:id/label` | Rename one iteration | | `PATCH` | `/v1/strategies/:id/session-label` | Rename a strategy session | **Custom indicators** | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/indicators` | Your saved custom indicators (name, source, columns, range) | | `GET` | `/v1/indicators/:name` | One saved indicator in full, including its Pine source | | `DELETE` | `/v1/indicators/:name` | Delete a saved indicator | | `POST` | `/v1/indicators` | Compile a Pine script against real data and save it: `{ "name", "pine_code" }`. *Plan gated (Pine)* | | `POST` | `/v1/indicators/validate` | Compile check a Pine script, no run, no save | | `POST` | `/v1/indicators/lint` | Offline Pine diagnostics (quota free) | | `POST` | `/v1/indicators/preview` | Compile and run a Pine indicator against real data (async → poll `GET …/preview/:jobId`) | | `POST` | `/v1/export/pine` | A `parsedQuery`, or an existing `backtestId`, → a TradingView Pine v6 `strategy()` script. *Power+* | | `POST` | `/v1/rephrase` | Rewrite a query into clearer, parser friendly phrasing (metered; identical repeats free) | **Market screener (crypto)** | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/screener/scan` | The tradable crypto universe with per token strength scores, price, 24h move and volume (cache fresh) | | `GET` | `/v1/screener/rsps` | Market regime verdict per timeframe: `RUN`, `REVIEW`, `SKIP` | | `GET` | `/v1/screener/token-stats` | Per token forward outcome base rates for a `tf` + signal `condition` | | `GET` | `/v1/screener/sectors` | Crypto sector performance (optional `date`) | | `GET` | `/v1/screener/rsps/matrix` | Pairwise dominance for the top `k` tokens | | `GET` | `/v1/screener/saved-scans` | Your saved screener filters: `POST` saves, `DELETE /:id` removes | | `GET` | `/v1/screener/scan-alerts` | Your scan alerts: `POST` creates, `DELETE /:id` removes; notified by Telegram/email. Every plan holds alerts: Free 3, Pro 25, Power 150, Quant and Enterprise unlimited | | `GET` | `/v1/screener/custom-benchmark` | Correlation + beta of the universe vs a `benchmark` you choose | | `GET` | `/v1/screener/token-multitf` | One token's recent OHLCV + multi TF screener rows (capped) | | `POST` | `/v1/screener/profiler` | Start the Perfect Token Profiler (async → poll `GET …/profiler/:jobId`) | **Webhooks** | Method | Path | Description | | --- | --- | --- | | `POST` | `/v1/webhooks` | Register an HTTPS receiver for `backtest.completed` / `backtest.failed`. `GET` lists, `DELETE /:id` removes | | `POST` | `/v1/webhooks/:id/test` | Fire one signed `webhook.test` event to verify your receiver | **Live trading & portfolio** A separate surface under `/v1/trading/*`: positions, orders, fills, closed trades, P&L, risk state, and controls that only ever reduce risk. It needs its own opt in key scopes and a live trading plan, so it has its own page: **[Trading & portfolio API](/docs/api/trading)**. ## GET /v1/backtests: query parameters Mix and match freely. Anything invalid comes back as a `400 invalid_query` that names the bad parameter. Nothing is ever silently ignored. | Parameter | Type (default) | Notes | | --- | --- | --- | | `limit` | int (20) | Page size, 1-100 | | `offset` | int (0) | Rows to skip (offset paging) | | `sort` | enum (created_at) | created_at, total_return, win_rate, total_trades, sharpe_ratio, max_drawdown, profit_factor, execution_time_ms | | `order` | enum (desc) | asc or desc | | `asset` | list (none) | One symbol or comma list, e.g. `BTCUSDT,ETHUSDT` | | `timeframe` | list (none) | One value or comma list, e.g. `4h,1d` | | `tag` | list (none) | One tag or comma list of your tags | | `search` | string (none) | Free text over your original query text (≤ 200 chars) | | `from` / `to` | ISO date (none) | Inclusive created_at window | | `status` | enum (completed) | completed, failed, running, pending, cancelled | | `include_stats` | bool (off) | Aggregate stats block (off by default for API keys) | | `include_facets` | bool (off) | Distinct assets/timeframes, for filter menus | ```text # newest first (default) /v1/backtests?limit=20 # best Sharpe on BTC or ETH, 4h, first half of 2025 /v1/backtests?asset=BTCUSDT,ETHUSDT&timeframe=4h&sort=sharpe_ratio&order=desc&from=2025-01-01&to=2025-06-30 # one tag, highest return first /v1/backtests?tag=breakout&sort=total_return&order=desc ``` ## REST quick start Three calls, end to end (parse, run, then read your history): ```bash # 1. parse plain English curl -X POST https://www.texttoquant.com/api/v1/parse \ -H "Authorization: Bearer ttq_..." -H "Content-Type: application/json" \ -d '{"query":"Buy BTCUSDT on the 4h when RSI(14) crosses above 30, sell at 6% profit or 3% stop loss, last 2 years"}' # 2. run it (bills one backtest) curl -X POST https://www.texttoquant.com/api/v1/backtests \ -H "Authorization: Bearer ttq_..." -H "Content-Type: application/json" \ -d '{"parsedQuery": }' # 3. read your results curl "https://www.texttoquant.com/api/v1/backtests?limit=10" \ -H "Authorization: Bearer ttq_..." ``` ## A successful run `POST /v1/backtests` returns summary metrics and a grade. Percentages are already scaled: `42.3` means 42.3%. ```json { "success": true, "data": { "analysisId": "a1b2c3d4-e5f6-…", "query": "Buy BTCUSDT on the 4h when RSI(14) …", "backtestResults": { "summary": { "totalReturnPct": 42.3, "winRate": 60.4, "sharpeRatio": 1.82, "maxDrawdownPct": 11.7, "profitFactor": 1.9, "totalTrades": 48 }, "grading": { "grade": "B+", "score": 78 } } } } ``` ### Probability scan Enterprise only: on other plans the field is absent. `GET /v1/backtests/:id` carries the full signal base rate at **`metrics.probabilityScan`**: the entry trigger measured across every time it fired, including the ones the run could not trade because a position was already open. ```json { "metrics": { "probabilityScan": { "totalSignals": 647, "resolvedSignals": 631, "pendingSignals": 16, "successRate": 27.7, "edge": { "breakevenRatePct": 26.1, "edgePts": 1.6, "expectancyPct": 0.026, "payoffRatio": 2.83, "ci95": { "low": 24.3, "high": 31.2 }, "verdict": "inconclusive", "minSampleForVerdict": 30 }, "execution": { "takenSignals": 544, "skippedSignals": 87, "skippedEdgePts": -1.4, "modelFidelity": { "compared": 544, "agreementPct": 91.2, "medianAbsDeltaPts": 0.31 } }, "successDefinition": "exit_targets", "unmodelledExits": ["indicator_signal"], "signalCount": 647 } } } ``` Reading it correctly matters more than reading it at all: - **`successRate` divides by `resolvedSignals`, never the total.** A pending signal is unknown, not unsuccessful. - **Never quote the rate without `edge.breakevenRatePct`.** 27% is excellent at a 3:1 payoff and fatal at 1:1; `edge.edgePts` is the difference and the actual claim. - **`edge.verdict` is decided by the interval, not the point estimate**: `edge` only when `ci95.low` clears breakeven, and `inconclusive` below `minSampleForVerdict` regardless. - **`execution.skippedEdgePts` is only a finding when the two intervals separate.** They usually overlap, and then the sample cannot say whether the position filter helped. - **`unmodelledExits`** lists exit types the scan did not simulate; **`execution.modelFidelity`** measures what that actually cost against the real trades. Everything here is gross of fees and position independent, a diagnostic of the trigger, not account P&L. The per signal rows are not in this payload; fetch them from the run's `chart-markers` dataset. `probabilityScan` is omitted on runs saved before 2026-08-22, and on heavy (chunked) backtests, which stream the series in slices and cannot run the scan. ## Cancel a queued run Start a run without waiting (`wait: false`) and you get a `202` with an `analysisId` to poll. Change your mind before it starts computing and you can cancel it, and the credit is refunded: ```bash # start async → 202 { "analysisId": "…", "status": "running" } curl -X POST https://www.texttoquant.com/api/v1/backtests \ -H "Authorization: Bearer ttq_..." -H "Content-Type: application/json" \ -d '{"parsedQuery": , "wait": false}' # cancel it, refunds the credit only if the run is still queued curl -X POST https://www.texttoquant.com/api/v1/backtests//cancel \ -H "Authorization: Bearer ttq_..." ``` The response says which case applied: ```json { "success": true, "data": { "analysisId": "…", "status": "cancelled", "refunded": true, "reason": "queued_removed" } } ``` `status` is `cancelled` (removed + `refunded: true`), `running` (already computing, not stopped, no refund), `completed` (already saved, nothing to cancel), or `not_found` (no cancelable job for you). A run that has already started keeps going server side and lands in your history. ## Custom indicators over the API Strategies can reference your **saved** custom indicators (from the Pine editor, CSV uploads, or `POST /v1/indicators`) in three steps: ```bash # 1. discover your saved indicator names GET /v1/indicators # 2. parse with hints so conditions are tagged custom POST /v1/parse { "query": "Buy BTCUSDT 1d when my_osc crosses above 0, from 2023-01-01 to 2025-01-01", "customIndicatorHints": ["my_osc"] } # 3. run with the name mapping POST /v1/backtests { "parsedQuery": { }, "savedIndicatorMapping": { "my_osc": "my_osc" } } ``` ## Webhooks: push instead of poll Register a receiver once and get an HTTPS POST the moment a run finishes (or fails). Registration returns the signing `secret` **once**; every delivery is signed so you can verify it came from us. ```text # register (max 3 active; https + public host only) POST /v1/webhooks { "url": "https://yourapp.com/ttq-hook", "events": ["backtest.completed"] } → { "id": "…", "secret": "whsec_…" } # store the secret NOW # every delivery is signed: # X-TTQ-Signature: t=,v1= # verify: recompute the HMAC over `${t}.${rawBody}` and compare constant-time # payload { "id": "evt_…", "event": "backtest.completed", "data": { "analysisId": "…", "status": "completed", "resultUrl": "/v1/backtests/…" } } ``` Deliveries retry 3× (0s / 30s / 120s). A receiver failing 20 times in a row is auto disabled. Reregister to resume. Prefer an AI agent? See the [MCP server](/docs/api/mcp). --- ## MCP server Source: https://docs.texttoquant.com/api/mcp Summary: Connect TextToQuant to Claude and other MCP clients. The Model Context Protocol (MCP) is an open standard, so the TextToQuant MCP server works with **any MCP compatible AI agent**, not just one. Claude, ChatGPT, Cursor, Cline, VS Code and other MCP hosts all connect to the same server. Ask it, in plain words, to build and test a strategy and it will parse, run, and read back the results. It uses the **same billing, plan limits, and account** as the app. There are two ways in. ## Hosted connector (OAuth) The hosted server URL is the same for every client: `https://www.texttoquant.com/api/mcp` You sign in with your TextToQuant account (no API key needed) and approve access on a consent screen. See and revoke every connected agent anytime from [Connected apps](/account/connections). The hosted connector works on **every plan**. It needs no API key and no separate entitlement. Tools that a plan does not include (the overfitting verdict on Pro, say) return a `plan_required` error at call time rather than being hidden, and runs bill the same credits as the app. API **keys**, for the REST surface and the downloadable server below, come with every paid plan, from Pro upward. ### One click install Add the server to your editor in one click, then sign in and approve access: - [Add to Claude](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=TextToQuant&connectorUrl=https%3A%2F%2Fwww.texttoquant.com%2Fapi%2Fmcp) - [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=texttoquant&config=eyJ1cmwiOiJodHRwczovL3d3dy50ZXh0dG9xdWFudC5jb20vYXBpL21jcCJ9) - [Add to VS Code](https://vscode.dev/redirect/mcp/install?name=texttoquant&config=%7B%22name%22%3A%22texttoquant%22%2C%22url%22%3A%22https%3A%2F%2Fwww.texttoquant.com%2Fapi%2Fmcp%22%2C%22type%22%3A%22http%22%7D) - [Add to LM Studio](lmstudio://add_mcp?name=texttoquant&config=eyJ1cmwiOiJodHRwczovL3d3dy50ZXh0dG9xdWFudC5jb20vYXBpL21jcCJ9) - [Add to Goose](goose://extension?type=streamable_http&id=texttoquant&name=TextToQuant&url=https%3A%2F%2Fwww.texttoquant.com%2Fapi%2Fmcp&description=Backtest%20trading%20strategies%20written%20in%20plain%20English) Claude **organization admins** can prefill the org wide dialog instead, which adds the connector for everyone in the workspace: [Add for the whole organization](https://claude.ai/admin-settings/connectors?modal=add-custom-connector&connectorName=TextToQuant&connectorUrl=https%3A%2F%2Fwww.texttoquant.com%2Fapi%2Fmcp). **Claude Code** takes one line, with no API key and no config file to edit: ```bash claude mcp add --transport http --scope user texttoquant https://www.texttoquant.com/api/mcp ``` Then run `/mcp` inside Claude Code to finish the browser sign in. `--scope user` registers the server for every project on the machine; drop it to add it to the current directory only. **Grok CLI** takes the same shape (no scope flag, it already writes the user level config): ```bash grok mcp add --transport http texttoquant https://www.texttoquant.com/api/mcp ``` **Gemini CLI** installs the TextToQuant extension, which wraps the same server plus a briefing the agent reads: ```bash gemini extensions install https://github.com/Youssef2784/texttoquant-gemini-extension ``` Then run `/mcp auth texttoquant` inside Gemini CLI to finish the browser sign in. **Claude Code plugin** is the alternative to the one line command above when you want the server *and* a TextToQuant skill (how to read a result honestly, when to stress test, what bills credits): ```bash claude plugin marketplace add Youssef2784/texttoquant-claude-plugins claude plugin install texttoquant@texttoquant ``` ChatGPT and other clients add the same URL by hand with the per client steps below. ### Claude [**Add to Claude**](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=TextToQuant&connectorUrl=https%3A%2F%2Fwww.texttoquant.com%2Fapi%2Fmcp) opens the **Add custom connector** dialog with the name and URL already filled in. Press **Add**, sign in with your TextToQuant account, and approve access. To do it by hand instead: **Settings → Connectors → Add custom connector**, and set the URL to `https://www.texttoquant.com/api/mcp`. It normally does not. The server publishes a `registration_endpoint`, so a client registers itself and no ID is typed. If a client will not register itself, open **Advanced** and set the **OAuth Client ID** to `61cc842b-17d0-478b-b3c0-01ce58366fa0` (public, the same for every user) with the **secret left empty**. ### ChatGPT ChatGPT reads remote MCP servers in **developer mode** (Plus, Pro, Business, Enterprise and Edu, on the web app). 1. **Settings → Connectors** (Apps), open **Advanced**, and turn on **Developer mode**. 2. Back in **Connectors**, **create a connector**: give it a name, set the **MCP Server URL** to `https://www.texttoquant.com/api/mcp`, and choose **OAuth** for authentication. 3. **Connect**, sign in with your TextToQuant account, and approve. 4. Start a chat, enable the connector, and ask it to run or show a backtest. In ChatGPT the `show_*` tools additionally render as an inline, interactive report widget. ### Cursor 1. **Cursor Settings → MCP → Add new MCP server** (or edit `~/.cursor/mcp.json` for a global server, or `.cursor/mcp.json` inside one project). 2. Add the remote server: ```json { "mcpServers": { "texttoquant": { "url": "https://www.texttoquant.com/api/mcp" } } } ``` 3. The first time you use it, Cursor opens a browser **OAuth** flow: sign in with your TextToQuant account and approve. Cursor stores the credentials for you. ### Grok Custom MCP connectors are available on every plan, free included, across web, iOS and Android. What each tool can do still follows your plan. 1. Go to **grok.com/connectors** and click **New Connector**, then **Custom**. 2. Enter `https://www.texttoquant.com/api/mcp` and complete the sign in. ### Mistral Le Chat Available on every plan, including free. 1. Side panel → **Intelligence → Connectors → + Add Connector**. 2. Open the **Custom MCP Connector** tab, enter the URL above, and authorize. ### Gemini **Gemini CLI**: install the extension (one line, above) and run `/mcp auth texttoquant` to sign in. It is also listed in the [Gemini CLI extensions gallery](https://geminicli.com/extensions). The Gemini app has no custom connector screen today; Gemini Enterprise admins add the server URL above through their connector settings. ### Perplexity and Microsoft Copilot These have no consumer facing custom connector screen today. Perplexity supports MCP on its enterprise plans, and Copilot through Copilot Studio with admin setup. In each case the server URL is the same one above; only the place you paste it differs. ### Other clients (Cline, VS Code, Zed and more) Add `https://www.texttoquant.com/api/mcp` as a remote MCP server and complete the OAuth sign in. A client that does not support remote OAuth connectors can use the downloadable server below with an API key: the public repo [texttoquant-mcp](https://github.com/Youssef2784/texttoquant-mcp) carries the server file, a README, and an `llms-install.md` an agent such as Cline can follow on its own. ### Where it is listed - **Official MCP Registry**: `com.texttoquant/texttoquant` at [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io/v0.1/servers?search=com.texttoquant%2Ftexttoquant), which feeds the GitHub MCP registry, VS Code and other catalogs. - **Gemini CLI extensions gallery**: [texttoquant-gemini-extension](https://github.com/Youssef2784/texttoquant-gemini-extension). - **Claude Code plugin marketplace**: [texttoquant-claude-plugins](https://github.com/Youssef2784/texttoquant-claude-plugins). - **Cursor plugin**: [texttoquant-cursor-plugin](https://github.com/Youssef2784/texttoquant-cursor-plugin) (server plus skill). - **Public server repo**: [texttoquant-mcp](https://github.com/Youssef2784/texttoquant-mcp) (downloadable server, `llms-install.md`). The Claude and ChatGPT directory listings, and the Docker MCP Catalog, are documented here as soon as they are live. ## Tools The server covers the whole workflow (**discover, author, run, read, refine, validate, and export**) plus the market screener and portfolios. You never call these by name: ask in plain language and the agent picks the right tool. Everything runs on your plan, billing, and account. Tools tagged **bills 1** spend one backtest credit; everything else is a free read. You don't need to memorize the list: the agent can call `describe_capabilities` to learn what the platform supports and `tools/list` to see every tool's schema. ### Discover & author | Tool | What it does | | --- | --- | | `describe_capabilities` | The platform's vocabulary (assets, timeframes, indicators, operators, exits, sizing) so the agent writes strategies that actually run | | `workspace_summary` | A one call "where was I" briefing: credits, recent runs with grades, indicators, live alerts | | `parse_strategy` | English → structured query | | `explain_parse` | The parsed strategy read back in plain English: confirm intent before spending a credit | | `rephrase_strategy` | Rewrite a vague idea into clearer, parser friendly phrasing (no invented parameters) | | `list_examples` | Browse the strategy gallery for a proven starting point (each card carries its `author`) | ### Run & read | Tool | What it does | | --- | --- | | `run_backtest` | Run a strategy, **bills 1**, idempotency supported | | `edit_backtest` | Fork a run with one change and rerun, keeping the iteration lineage, **bills 1** | | `cancel_backtest` | Stop a queued run and refund its credit, if it hasn't started computing | | `list_backtests` | Find runs server side: filter by asset, timeframe, tag or text, sort by any metric | | `get_backtest` | One run in full, by id | | `explain_grade` | Why the grade: the four scored pillars, weights and warnings, and what to improve | | `get_backtest_data` | Equity, trades, candles, Monte Carlo, plus the entry/exit condition logs (why a signal fired, or never did) | | `compare_backtests` | Compare 2-4 runs side by side as one chart | | `get_iterations` | The edit history of a strategy session, run by run | | `diff_iterations` | Compare two runs: exactly which parsed strategy fields changed and the metric deltas, "what did I change and did it help?" | | `organize_history` | Tag runs, rename iterations and sessions, keep history tidy | | `manage_webhooks` | Push instead of poll: signed HTTPS callbacks when runs finish | ### Refine by market regime | Tool | What it does | | --- | --- | | `refine_context` | Rebucket a run's trades along different regime dimensions, no rerun, no credit | | `filter_by_context` | Rerun entering only inside a chosen regime, to prove an edge is real, **bills 1** | | `get_context_trades` | The individual trades behind a regime, as evidence | ### Validate & stress test | Tool | What it does | | --- | --- | | `start_analysis` | Sweep, grid, walk forward, multi asset, **variants** (generate + rank strategy twists), **bills 1+** | | `get_analysis` | Poll a job, or read the market regime context insights | | `show_analysis` | Render sweep / heatmap / walk forward / variants views | | `analyze_conditions` | Forward outcome base rates for one entry condition, a filter probe, not a backtest | | `get_overfit_verdict` | The Deflated Sharpe + PBO overfitting verdict for a run | | `get_regime_edge` | The Robustness "Edge by regime": per regime P&L, win rate and a dependence verdict (Trend Up/Down, Quiet Range, Volatile Chop) | ### Custom indicators (Pine, JavaScript, Python or CSV) Indicators can be authored in four ways: a Pine script compiled through TradingView, JavaScript or Python run in an isolated sandbox against real bars, or precomputed values uploaded as CSV. `list_indicators` and `get_indicator` cover all of them; `delete_indicator` removes any saved indicator regardless of language. | Tool | What it does | | --- | --- | | `list_indicators` | Your saved custom indicators | | `get_indicator` | One indicator in full, including its source | | `validate_indicator` | Compile check a Pine script before saving | | `lint_indicator` | Offline Pine diagnostics (quota free) | | `preview_indicator`, `get_indicator_preview` | Compile and run a Pine indicator against real data, then poll the result | | `save_pine_indicator` | Compile and save a Pine script | | `validate_js_indicator` | Check a JavaScript indicator's shape (`INPUTS`/`PLOTS`/`calc`) without running it, no bars loaded, instant and free | | `preview_js_indicator` | Compile and run a JavaScript indicator against real bars and return the plotted series directly, saves nothing | | `save_js_indicator` | Author an indicator in JavaScript and save it; run against real bars before anything is stored | | `validate_py_indicator` | Check a Python indicator's shape without running it, no bars loaded, instant and free | | `preview_py_indicator` | Compile and run a Python indicator against real bars and return the plotted series directly, saves nothing | | `save_py_indicator` | Author an indicator in Python and save it; same capability as `save_js_indicator`, numerically identical | | `save_csv_indicator` | Save an indicator from precomputed values supplied as CSV text, for a series the platform can't compute itself | | `delete_indicator` | Remove a saved indicator | A run that uses a saved indicator is stored the way the terminal writes it: the query text carries an `@"Name"` tag for every indicator the run resolved, and the row records which saved sheet each name bound to. Opening that run from **History** turns on **My Indicators**, attaches those sheets, and shows the tags in the query box, so it can be edited and rerun without retyping anything. Pass the names as `customIndicatorHints` to `parse_strategy` (or `savedIndicatorMapping` to `run_backtest`) and this happens automatically. ### Export | Tool | What it does | | --- | --- | | `export_pine` | Turn a strategy, or an existing run by id, into a TradingView Pine v6 script, with anything Pine can't express listed explicitly | ### Market screener (crypto) | Tool | What it does | | --- | --- | | `scan_market` | The tradable universe with per token strength scores and the top movers right now | | `market_regime` | The market's risk on / mixed / risk off verdict per timeframe | | `token_stats` | Per token forward outcome base rates for a signal | | `market_sectors` | Which crypto sectors are leading or lagging | | `rsps_matrix` | Pairwise dominance for the top tokens (advanced) | | `saved_scans` | Save, list and delete your screener filters | | `list_scan_alerts`, `create_scan_alert`, `update_scan_alert`, `delete_scan_alert` | "Notify me when tokens match…" alerts: create, pause/resume or change the condition without deleting, and remove; delivered by Telegram or email (every plan holds some; the number of ACTIVE alerts is capped per plan) | | `custom_benchmark` | Correlation and beta of the universe vs a benchmark you choose | | `token_multitf` | One token's recent OHLCV + multi timeframe screener rows | | `get_candles`, `list_markets` | Raw OHLCV for any symbol/timeframe, and the tradable market universe | | `run_profiler`, `get_profiler` | Perfect Token Profiler: what the top movers looked like before they ran | ### Portfolios | Tool | What it does | | --- | --- | | `parse_portfolio` | Split a multi asset prompt into per asset strategies | | `run_portfolio` | Run a shared capital multi asset portfolio, **bills one per asset** | | `list_portfolios` | Your saved book runs, newest first, find one you ran earlier (the durable run id) | | `get_portfolio` | Poll a portfolio run | | `show_portfolio` | Render a completed book's report in chat: metric strip, equity + drawdown vs an equal weight basket, per asset attribution | | `get_portfolio_analysis` | A completed book's analysis: grade, per asset attribution, benchmark, Monte Carlo, walk forward & out of sample | | `run_portfolio_sweep`, `get_portfolio_sweep` | Rerun a book across 2-5 values of one knob to test robustness, **bills K×N** (K values × N assets), idempotency supported | | `cancel_portfolio_sweep` | Stop a running sweep, every book not yet run is refunded | | `share_portfolio` | Mint / revoke a portfolio run's share link | ### Present & share | Tool | What it does | | --- | --- | | `show_backtest` | Render a run's report in chat | | `show_context` | Render the market regime dashboard | | `share_backtest` | Mint / revoke a share link | | `publish_to_gallery` | List (or unlist) one of your shared runs in the public showcase gallery `list_examples` reads from | | `get_usage` | Plan tier and credits left | | `get_plans` | Tiers, current pricing, feature/limit deltas and the upgrade URL, what the next tier costs and unlocks when you hit a wall | | `get_notification_channels` | Which channels (Telegram/email) will deliver alerts, plus the setup link to connect one | Tool results are **HTML first**: on clients that render inline HTML (ChatGPT today, more as they adopt the MCP Apps standard) the `show_*` tools and the data tools above return a fully interactive TTQ dark dashboard: price candles with entry/exit markers, equity & drawdown, Monte Carlo, regime rankings, attribution bars, verdict banners and sortable tables. On clients that don't yet render HTML (Claude's connector today) the same tools automatically fall back to chart images + text, so nothing breaks. The moment a client declares HTML support it gets the interactive views with no change on your side. Ask the agent to “show my last backtest”, then ask for a share link to open the full report in any browser. Clients that pass an MCP `progressToken` (Claude and others) receive real `notifications/progress` events during `run_backtest`: stage and percent streamed as the worker executes. Scripted REST callers get the same via `wait: false` + `GET /v1/backtests/:id/status`. New to the portfolio tools? `parse_portfolio` and `run_portfolio` drive a shared capital, multi asset book. See the [Portfolios reference](/docs/reference/portfolio) for how capital, contention and the book report work. ### Live trading & portfolio Everything above is research. These tools read and act on a **live trading account**, and they are governed differently from the rest of the surface. See [Trading & portfolio API](/docs/api/trading) for the same capabilities over plain HTTP. **Reads** require the `mcp:trading:read` scope. | Tool | What it does | | --- | --- | | `get_accounts` | Connected exchange accounts paired with their balances. Where every `connectionId` comes from | | `get_positions` | Open positions: side, size, entry, unrealized P&L, leverage, liquidation price | | `get_orders` | Resting orders, or terminal ones with `history: true` | | `get_fills` | The execution feed: what actually happened, as opposed to what was asked for | | `get_closed_trades` | The closed trade ledger **and** its performance summary in one call: realised P&L, win rate, profit factor, expectancy | | `get_portfolio_snapshot` | Point in time equity, allocated capital and open exposure | | `get_portfolio_equity` | The equity curve through time | | `get_portfolio_pnl` | Where the money came from: `daily`, `breakdown` (symbol/side/hour/weekday) or `attribution` (per strategy) | | `get_risk_state` | Risk budget, armed kill switches and quarantined deployments: the read that tells a stopped account from a quiet one | | `get_deployments`, `get_runtime_health` | What is running, and whether it is actually running | | `get_symbol_info` | The venue's own `tickSize` / `stepSize` / `minNotional` / maintenance margin rate | **Writes** require `mcp:trading:write`, and every one asks you to confirm before it acts. | Tool | What it does | | --- | --- | | `pause_deployment` | Stop a strategy acting on new signals (`entriesOnly: true` blocks entries but keeps exits armed) | | `resume_deployment` | Undo a pause | | `close_position` | Close a deployment's position, or an account position by `connectionId` + `symbol` + `side` | | `close_all_positions` | Flatten the book | | `cancel_orders` | Cancel one resting order, or all of them | | `set_position_protection` | Attach or move an exchange side stop loss / take profit | | `activate_kill_switch`, `release_kill_switch` | Halt trading over a deployment, an account, or everything | There is no tool that opens a position, places a resting order, or moves funds: not hidden behind a flag, not gated on a scope. **Not present.** Every write above pauses, cancels, closes, protects or halts, so the worst an agent can do with your account is leave it flat, protected or paused. Three controls stack on top of that, and none substitutes for the others: the tools are **hidden** from `tools/list` unless the operator enables them; the `mcp:trading:*` scopes are **fail closed**, so unlike the research scopes they are never satisfied by a token that simply carries none; and every write requires an **explicit human confirmation** that is also fail closed: a client that cannot show you a prompt does not get to act. **Cancelling is not closing**: `cancel-all` leaves every position open, and possibly unprotected. **Halting is not closing**: a kill switch stops new activity; open positions stay open. **Closing is not stopping**: after `close_all_positions` the strategies keep running and may reenter. Each tool says so in its own result, but it is worth knowing before you ask. ## Resources Beyond tools, the server exposes your data as MCP **resources**, so a capable client can pull a run or indicator straight into context (an `@` mention, an attach menu) without a tool call: | URI | What it returns | | --- | --- | | `backtest://{id}` | A saved run as JSON: summary metrics, grade, effective parameters, honesty flags | | `indicator://{name}` | A saved custom indicator: metadata, plot names, and its Pine source | | `docs://{slug}` | The platform's interpretation doctrine (`metrics`, `robustness`, `grading`), so the agent reads results TextToQuant's way, honesty rules included | `resources/list` returns your recent runs and indicators (cursor paginated) and `resources/templates/list` advertises both URI shapes, so a client can construct a link to any run or indicator you own. Everything is scoped to your account. ## Protocol support The hosted server is a full MCP resource server over the streamable HTTP transport, so capable clients get the whole protocol surface, not just tool calls: - **Cancellation**: cancel an in flight tool call; a queued backtest is stopped and its credit refunded. - **Resumable progress**: a dropped `run_backtest` stream reconnects with `Last-Event-ID` and replays the events it missed, then keeps following the run on any server instance. - **Argument completion**: `completion/complete` autocompletes backtest ids and indicator names as you type. - **Resource subscriptions & list changed** notifications, and **logging** (`logging/setLevel` → `notifications/message`), for clients that implement them. - **Least privilege scopes**: the server enforces coarse `mcp:read` / `mcp:run` / `mcp:write` scopes per tool when a token carries them, so a connection can grant an agent read only or no spend access. The OAuth flow itself requests only `openid email`. When enabled on the server, clients that support MCP **elicitation** are asked to confirm the credit spend before a billed tool (`run_backtest`, `edit_backtest`, `filter_by_context`, `run_portfolio`, `start_analysis`) actually runs, a human in the loop check on top of your plan limit and the per agent daily spend cap. ## Downloadable server Prefer to run the server yourself, or use a local MCP client? Download it and point your agent config at it with your API key. The download is built from the same code as the hosted server, so it carries the **full tool surface** above (the previous 4 tool build stays at `/api/v1/mcp-server-legacy.mjs`). ```bash curl -o ttq-mcp.mjs https://www.texttoquant.com/api/v1/mcp-server.mjs # or clone it, with a README and llms-install.md an agent can follow: git clone https://github.com/Youssef2784/texttoquant-mcp ``` ```json { "mcpServers": { "texttoquant": { "command": "node", "args": ["/path/to/ttq-mcp.mjs"], "env": { "TTQ_API_KEY": "ttq_...", "TTQ_API_BASE": "https://www.texttoquant.com/api" } } } } ``` The [API keys](/account/api) page has this config ready to copy, prefilled with your key. ### Live trading on the downloadable server The trading tools are hidden here too, and stay hidden unless you set `MCP_TRADING_TOOLS_ENABLED=1` in that `env` block. Even then the API key it carries needs the `trading:read` / `trading:write` [scopes](/docs/api/authentication#key-scopes), which no existing key has: turning the tools on does not turn access on. The confirmation is identical to the hosted connector, and identically fail closed: before any write the server sends your client an MCP `elicitation/create` request and refuses unless you explicitly approve. A client that never declared elicitation support cannot prompt you, so it **cannot act**: the call is refused with an explanation rather than run silently. Prefer calling the API directly? See the [REST endpoints](/docs/api/endpoints) and the [Trading & portfolio API](/docs/api/trading). --- ## Versioning and deprecation Source: https://docs.texttoquant.com/api/versioning Summary: What v1 guarantees, how changes are announced, and how much notice you get. The public API is versioned in its path: every endpoint lives under `/api/v1/`. That version is a promise about compatibility, and this page is what the promise actually says. ## What v1 guarantees While `v1` is the current version, we will not: - remove an endpoint, or change its HTTP method or path - remove a field from a response, or change the type of an existing field - add a new **required** request parameter - change the meaning of an existing field without renaming it - tighten validation so that a request which succeeded yesterday fails today ## What is not a breaking change These can land at any time, so your client must tolerate them: - **New fields in a response.** Parse permissively and ignore what you do not recognise. This is the single most common way an integration breaks on a change that was not breaking. - **New optional request parameters.** - **New endpoints, and new enum values** in a field documented as extensible. - **New MCP tools**, or new optional arguments on an existing tool. - **Ordering** of items in a response, unless the endpoint documents a sort. - **Error message wording.** Branch on the `code` field, never on the prose in `error`. The prose is written for humans and gets improved. ## Deprecation When something has to go, it goes in stages: 1. **Announced.** The [changelog](/changelog) records it, with the replacement and the date. The changelog has an [RSS feed](/changelog/feed.xml). 2. **Marked.** Affected responses carry a `Deprecation` header and, where a date is set, a `Sunset` header ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)). MCP tool responses carry the same notice inline so an agent sees it. 3. **Removed.** No sooner than **90 days** after the announcement, and never inside `v1`. A removal means a new version. If a change is forced on us by a security issue or an upstream provider, we may have to move faster. In that case we will say so explicitly and contact affected accounts directly rather than relying on you reading a changelog. ## Version 2 There is no v2. If one is introduced, `v1` will keep serving for at least **12 months** after v2 becomes generally available, and the migration will be documented endpoint by endpoint rather than as a single "rewrite your client" note. ## MCP The hosted MCP server is versioned separately from the REST API, because MCP clients negotiate capabilities rather than pinning a URL. The same rules apply in spirit: tools are not removed or given new required arguments without the notice period above, and the live tool catalog is published at [`/api/v1/mcp/tools.json`](https://www.texttoquant.com/api/v1/mcp/tools.json) so you can diff it yourself. ## Staying informed - [Changelog](/changelog) and its [feed](/changelog/feed.xml) - The [OpenAPI 3.1 contract](https://www.texttoquant.com/api/v1/openapi.json), which is generated from the live routes - `security@texttoquant.com` for anything urgent --- # Academy ## Track: Foundations ### Your first strategy Source: https://www.texttoquant.com/academy/first-strategy Teaches: How a plain English sentence becomes a backtest Level: Beginner · 4 min You don't write code. You describe the rules, and TextToQuant compiles your sentence into an exact, deterministic strategy and runs it on real history. Let's build the simplest possible one. #### You describe, the engine compiles Type what you want in plain English. The engine turns it into a precise set of rules: an entry, an exit, a market, a timeframe, with no hidden decisions. Before it runs, it shows you the parsed rules. Read them, because that is exactly what's being tested. #### The hello world of trend following A moving average crossover is the classic starting point. When a faster average crosses above a slower one, recent prices have pulled ahead of older ones, which is a simple, mechanical definition of 'an uptrend started'. Run it and watch where it enters and exits on the chart. #### Every strategy needs a window Notice the sentence ends with a timeframe and a window: daily bars, the last 3 years. The engine will not run without a window, because a backtest is always a claim about one specific stretch of history. Leave it out and the terminal stops and asks for one. Say it up front and you also make the result honest: three years of daily bars is what this number is about, nothing more. Runnable strategy: ``` Buy BTC when the 50 day moving average crosses above the 200 day, sell when it crosses back below, on 1D, last 3 years. ``` Takeaway: Read the parsed rules it shows you before running. That is the exact strategy being tested, with no hidden AI decisions. --- ### Reading the parsed rules Source: https://www.texttoquant.com/academy/parsed-rules Teaches: Why there are no hidden AI decisions Level: Beginner · 4 min The single most important habit in the whole platform: before you trust any result, read the rules the engine parsed from your sentence. This is what makes a backtest here reproducible instead of a black box. #### Plain English is the source, the rules are the truth Your sentence is the input, but the parsed rules are what actually run. Every condition (the indicator, the comparison, the timeframe, the direction) is spelled out explicitly. If the parse doesn't match what you meant, you edit the sentence, not some invisible setting. #### Ambiguity resolves conservatively Where a sentence could be read two ways, the engine tells you how it read it, and where it will not guess (a 'breakout', say) it stops and asks you to pick. Run this two condition example and check the parse: it should require RSI(14) below 35 AND price above the 200 day average, a dip inside an uptrend, not either one alone. The 14 is an assumption you never typed, and the parse says so. #### Same input, same output, always Because there's no randomness in the parse or the fill logic, the same sentence on the same data gives the same result every time. That determinism is what lets you compare two versions of an idea and trust the difference is your change, not noise. Runnable strategy: ``` Buy BTC when RSI is below 35 and the price is above the 200 day moving average; sell when RSI rises above 65, on 1D, last 3 years. ``` Takeaway: If the result surprises you, read the parse first. Nine times out of ten the strategy did exactly what you wrote, just not what you meant. --- ### Indicators & signals Source: https://www.texttoquant.com/academy/indicators-and-signals Teaches: The building blocks: trend, momentum, and bands Level: Beginner · 5 min Almost every strategy is built from a small vocabulary of indicators. Learn what each family measures and you can describe most ideas without ever looking up a formula. #### Trend vs momentum Trend indicators (moving averages) tell you which way price has been leaning. Momentum oscillators (RSI, MACD) tell you how fast it's moving and whether that speed is fading. Trend answers 'which direction?', momentum answers 'is the move still strong?'. #### Signals are events, not states A good entry is an event: a cross, a breakout, a threshold being crossed. It is not a condition that's simply true for weeks. 'MACD crosses above its signal line' fires once; 'MACD is positive' can be true for a month. Events keep you from re entering the same trade every bar. #### Combine to be selective One indicator is a blunt instrument. This example only buys when momentum turns up (a MACD cross) and price already has strength (RSI above 50). Together those two conditions trade far less often, but on cleaner setups. Runnable strategy: ``` Buy BTC when MACD crosses above its signal line and RSI is above 50; exit when MACD crosses back below its signal line, on 1D, last 3 years. ``` Takeaway: Prefer event style signals (crosses, breakouts) over state style signals, and combine a trend check with a momentum check to trade fewer, better setups. --- ### Entries, exits & stops Source: https://www.texttoquant.com/academy/entries-exits-stops Teaches: Every strategy needs a way in and a way out Level: Beginner · 5 min An entry rule is only half a strategy. How you exit (a target, a stop, a signal, or a trailing stop) often matters more for the final result than how you got in. #### Three ways out You can exit on a signal (the opposite of your entry), at a fixed target or stop (a price level), or with a trailing stop (a level that follows price up). Most strategies use a combination: a stop to cap the loss and either a target or a signal to book the win. #### Stops define your risk The stop loss is where you admit the idea was wrong. It sets the maximum you can lose on the trade, which (as the Risk & Execution track shows) is what lets you size the position sensibly. A strategy without a stop has undefined risk. #### Targets vs letting it run A fixed take profit raises your win rate but caps big winners. A trailing stop gives back a little at the end of every trade but lets the occasional runner pay for many small losses. This example uses both a stop and a target so you can see the trade off on the chart. #### When the engine asks instead of guessing 'Breaks above the 20-day high' can mean three different rules, so the engine stops and asks which one you meant before it parses. Pick 20-day high: a close above the highest high of the previous 20 days. That pause is the product refusing to choose for you, and it is the same honesty you will lean on when a result looks too good. Runnable strategy: ``` Buy ETH when price breaks above the 20-day high; exit at a 15% take profit or a 7% stop loss, whichever comes first, on 1D, last 3 years. ``` Takeaway: Design the exit as deliberately as the entry: a stop caps the loss, and the choice between a fixed target and a trailing stop is really a choice about how you want your winners to look. --- ## Track: Validation ### The overfitting trap Source: https://www.texttoquant.com/academy/overfitting-trap Teaches: Why a great backtest can be a lie Level: Beginner · 5 min It's easy to find parameters that looked perfect in the past. The hard part is knowing whether you found a real edge or just fit the noise. This is the most important lesson in the Academy. #### The past is easy to fit Any market has thousands of coincidences. Search hard enough and you'll find an RSI level and a timeframe that would have printed money on that exact history. That doesn't mean it will repeat; it means you found a pattern in the noise. #### The sweep exposes it Run this, then open Robustness and Parameter sensitivity (on Power and above). Sweep the RSI levels and watch the Deflated Sharpe on the board fall away from the raw Sharpe: it discounts your result by how many settings you tried. A big gap between the two is the tell that the edge was mostly search luck. The overfit probability needs a grid of settings, so run a Joint sweep when you want that number as well. Runnable strategy: ``` Buy ETH when RSI drops below 30, sell when RSI rises above 70, on the 4 hour chart, last 3 years. ``` Takeaway: Never trust a single backtest. A robust edge survives the sweep; an overfit one collapses under the Deflated Sharpe. --- ### Out of sample is the real test Source: https://www.texttoquant.com/academy/out-of-sample Teaches: Holding data back to catch curve fitting Level: Beginner · 5 min If overfitting is the disease, out of sample testing is the diagnosis. The idea is simple: judge the strategy on data it was never allowed to see. #### Hold the last slice out The trust strip under every result carries an OOS chip. The engine scores the last 30% of bars separately from the first 70% and grades whether the edge persisted late in the window. Nothing is fitted here, it is a plain split, but a strategy that only made money early in the window is exactly the kind of coincidence this catches. Hover the chip for the late slice's return and trade count. #### Walk forward is the strict version For the toughest test, open Robustness and Walk forward optimization, on Power and above. It optimizes a parameter on an in sample window and scores the winner only on the unseen window that follows, then rolls forward and does it again. That is the closest a backtest gets to how you would actually run and retune the strategy live. Runnable strategy: ``` Buy SOL when price crosses above the 50-day SMA, exit with a 10% trailing stop, on 1D, last 3 years. ``` Takeaway: In sample performance is a hypothesis. Out of sample and walk forward are how you test it. --- ### Monte Carlo & robustness Source: https://www.texttoquant.com/academy/monte-carlo Teaches: How much of your result is luck? Level: Intermediate · 6 min Your equity curve is one path through history, the specific order the trades happened to arrive in. Monte Carlo asks: how different could it have been, and how bad a drawdown should you actually plan for? #### One curve is one sample The same set of trades in a different order produces a different looking equity curve and a different maximum drawdown. Judging a strategy on the single historical ordering overstates how much you know about it. #### Resample a thousand times Run this, then open the Monte Carlo risk analysis, on Pro and above. It reshuffles and resamples the trades a thousand times to build a distribution of outcomes. A robust edge stays profitable across most reorderings; a fragile one depends on a lucky sequence. #### Plan for the tail, not the average The most useful output isn't the median. It's the drawdown you'd face in a bad but plausible run. If the 95th percentile drawdown is deeper than you could stomach, the strategy is too risky for you even if its average looks great. Runnable strategy: ``` Buy BTC when the 20 day moving average crosses above the 50 day; sell when it crosses back below, on 1D, last 3 years. ``` Takeaway: Size and judge a strategy by its bad runs, not its lucky one. Monte Carlo turns a single equity curve into the range of outcomes you should actually expect. --- ### Deflated Sharpe & the multiple testing tax Source: https://www.texttoquant.com/academy/deflated-sharpe Teaches: Why trying 100 ideas inflates the best one Level: Intermediate · 6 min The more strategies you try, the better your best one looks, for reasons that have nothing to do with skill. The Deflated Sharpe Ratio puts a number on that penalty. #### Selection inflates the winner Flip enough coins and one 'lucky' coin will land heads ten times in a row. Backtest enough variations and one will post a gorgeous Sharpe by chance alone. Reporting only the winner, without counting the losers you discarded, is how backtests lie. #### The Deflated Sharpe discounts the search The Deflated Sharpe Ratio adjusts a strategy's Sharpe down based on how many trials it took to find it and how noisy those returns were. It answers the honest question: given everything I tried, what's the chance this edge is real? #### Count every trial The tax only works if you're honest about the trial count. Every tweak to the RSI level, every timeframe you tried, every asset you swapped in is a trial. Run this, then open Robustness and Parameter sensitivity (on Power and above), and note how the raw Sharpe and the Deflated Sharpe diverge as the recorded trials pile up. Runnable strategy: ``` Buy SOL when RSI crosses above 55 on the 4 hour chart; exit when RSI falls below 45, last 3 years. ``` Takeaway: Your edge is the Deflated Sharpe, not the raw one. If it survives being discounted for every idea you tried, it's worth taking seriously. --- ## Track: Risk & Execution ### Position sizing & risk per trade Source: https://www.texttoquant.com/academy/position-sizing Teaches: How much to bet is bigger than what to bet on Level: Intermediate · 6 min The same entry signal can be a disaster or a winner depending on how you size and where you stop. Position sizing is quietly the biggest lever on the shape of your equity curve. #### Risk a fixed fraction, not a fixed quantity Professional sizing risks the same small percentage of equity on every trade, say 1%. Because your stop defines how far price can move against you, the stop distance sets the position size automatically: wider stop, smaller position, same dollar risk. This is also the engine's default when you say nothing about size. #### Volatility aware stops A fixed 5% stop is too tight for a wild market and too wide for a calm one. An ATR stop places the stop a multiple of the market's typical range away, so risk stays consistent across assets and regimes. This example risks 1% with a 2x ATR stop. #### Feel the drawdown Run this, then find Position size in the parsed strategy. It reads 1% risk per trade. Change it to 3%, run again, and watch how sizing, not the entry, controls the depth of the drawdown you'd actually have to survive. The entries are identical in both runs. A great signal you can't hold through its drawdown earns you nothing. Runnable strategy: ``` Buy BTC on a MACD bullish cross, risk 1% per trade with a 2 ATR stop and a 3:1 take profit, on 1D, last 3 years. ``` Takeaway: A mediocre edge with great risk control beats a great edge you can't hold through the drawdown. --- ### Stops, targets & R multiples Source: https://www.texttoquant.com/academy/stops-targets-r-multiples Teaches: Thinking in R instead of dollars Level: Intermediate · 5 min Once every trade risks the same fraction of your account, you can stop thinking in dollars and start thinking in R, units of risk. It makes wins, losses, and whole strategies directly comparable. #### R is your unit of risk If a trade risks 1% and makes 2%, that's +2R. A stop out is -1R. Suddenly a strategy isn't 'up $4,300'; it's 'averaging +0.3R per trade over 200 trades', which tells you far more about whether the edge is real and repeatable. #### The reward to risk choice A 2R target means you risk one unit to make two. Higher targets lower your win rate but raise the payoff per win; the two trade off. Expectancy (win rate times average win, minus loss rate times average loss, all in R) is what actually matters. #### Let ATR set both ends This example sets the stop at 2x ATR and the target at 4x ATR, a clean 2R trade that adapts to volatility on both ends. The entry is a plain RSI cross so nothing distracts from the exits. Run it and read the average R multiple in the trade metrics, not just the dollar total. Runnable strategy: ``` Buy BTC when RSI crosses above 50 on the daily chart; set the stop at 2 ATR and the take profit at 4 ATR, last 3 years. ``` Takeaway: Judge trades in R, not dollars. A positive expectancy in R that survives validation is an edge; a big dollar number without it is just a big position. --- ### Confirm across timeframes Source: https://www.texttoquant.com/academy/multi-timeframe Teaches: Higher timeframe context filters bad trades Level: Intermediate · 5 min A signal is stronger when the bigger trend agrees with it. A multi timeframe filter keeps you from fighting the dominant trend, one of the cheapest ways to improve a strategy's quality. #### Trade with the bigger trend This strategy only takes long trades when the bigger trend is up: it buys a momentum turn on the daily chart, but only while the close is above the 200 day SMA, the slow line most traders use for the long trend. It simply refuses to buy dips inside a larger downtrend, where so many entries go to die. On Power and above you can split the two across timeframes, a 1 hour entry with a daily filter (write 'daily' for both the close and the average so the parse reads a daily leg rather than a 200 hour one); the run below keeps both on one chart so it works on every plan. #### No look ahead, by construction TextToQuant resolves the daily leg on its own aligned series and only ever reads completed higher timeframe bars. A 1 hour strategy consulting the daily trend sees yesterday's finished daily bar, never today's still forming one, so the filter can't cheat by peeking at the future. #### Fewer trades, better ones A filter's job is to say no. Run this, then delete the 'but only while' clause and run it again. Compare the two trade counts and win rates: the filtered version should take materially fewer trades, on setups where the wind was at your back. Runnable strategy: ``` Buy ETH on the daily chart when RSI crosses above 40, but only while the close is above the 200 SMA; exit when RSI crosses below 50, last 3 years. ``` Takeaway: Filters trade fewer, better setups. Compare this run's trade count and win rate to the same rules without the daily filter. --- ### Execution realism Source: https://www.texttoquant.com/academy/execution-realism Teaches: The gap between a backtest and a real account Level: Intermediate · 6 min A backtest here is never free: by default every fill is charged a 0.05% taker fee and 0.02% slippage, roughly what a crypto market order costs. A real account can pay more, through a wider spread, market impact, or a worse venue. For high frequency strategies, that friction is the whole game. #### Every fill costs something Fees are charged on entries and exits alike; slippage means you get a slightly worse price than the signal showed. The parse shows both assumptions next to the rules, and the trust strip under the result repeats which frictions the run was scored with, so a frictionless number can never pass as a real one. #### Turnover multiplies the cost A strategy that trades once a month barely notices fees. One that trades ten times a day pays that friction hundreds of times. The more a strategy trades, the more of its paper edge gets eaten, which is why many beautiful high frequency backtests are unprofitable live. #### Stress it, then decide Run this, then open Execution realism, raise the maker and taker fees, add a bid/ask spread, and run again. Watch the edge shrink. If a modest, realistic cost assumption erases the profit, the strategy was never real; it was living in the friction you forgot to charge. Runnable strategy: ``` Buy BTC when the 10 day moving average crosses above the 30 day, sell on the reverse cross, on 1D, last 3 years. ``` Takeaway: Always stress test an edge with realistic fees and slippage before believing it. The strategies that survive higher costs are the ones worth trading. --- # Glossary Source: https://www.texttoquant.com/academy/glossary ## Moving average (MA) The average price over the last N bars, recomputed each bar. A basic trend filter. A rising MA means recent prices sit above older ones. The cross of a fast MA above a slow one is the classic 'hello world' trend following signal. See also: macd, breakout ## RSI (Relative Strength Index) A 0 to 100 momentum oscillator measuring the speed of recent gains vs losses. Readings under ~30 are often called 'oversold' and over ~70 'overbought'. But in a strong trend RSI can stay pinned at an extreme for a long time, so it works best paired with a trend filter. See also: macd, multi-timeframe ## MACD The gap between a fast and a slow moving average, plus a signal line. A MACD line crossing above its signal line reads as momentum turning up; a cross below reads as momentum turning down. It is a smoother, slower cousin of a raw MA crossover. See also: moving-average, rsi ## ATR stop A stop loss placed a multiple of Average True Range away from entry. ATR measures a market's typical bar range, so an ATR stop adapts to volatility (wider in wild markets, tighter in calm ones) instead of a fixed percentage that ignores conditions. See also: r-multiple, position-sizing ## Overfitting (curve fitting) Tuning a strategy so tightly to past data that it captures noise, not a real edge. An overfit strategy looks brilliant in the backtest and falls apart live. The more parameter combinations you try, the easier it is to find one that fit the past by pure luck. See also: deflated-sharpe, out-of-sample, walk-forward ## Deflated Sharpe Ratio (DSR) A Sharpe ratio discounted for how many strategies you tried. Test 100 variations and keep the best, and its Sharpe is inflated by selection alone. DSR estimates the probability the edge is real after accounting for that search. A big drop from the raw Sharpe means the result was mostly luck. See also: sharpe-ratio, overfitting ## Out of sample (OOS) Data held out of the fitting process, used only to judge the finished strategy. If a strategy holds up on data it 'never saw', the edge is more likely real than lucky. In sample performance is a hypothesis; out of sample is the test. See also: walk-forward, overfitting ## Walk forward optimization Refitting parameters on rolling in sample windows, scored only on the next forward window. The strictest out of sample test. It mimics retuning a live strategy over time and only ever grades it on data that came after the fit. See also: out-of-sample, overfitting ## Monte Carlo simulation Reshuffling or resampling trades thousands of times to see a range of outcomes. One equity curve is a single path through history. Monte Carlo shows how much of your result came from the specific order of trades versus a repeatable edge, and how deep a drawdown you should expect. See also: max-drawdown, out-of-sample ## R multiple A trade's result expressed in units of the risk taken (R). Risk 1% and make 2% and that's +2R. Thinking in R makes wins and losses comparable across trades regardless of position size, and turns 'win rate' into 'expectancy per R'. See also: position-sizing, atr-stop, take-profit ## Position sizing How much to buy, often set so a stop out loses a fixed % of equity. Risk based sizing ties position size to your stop distance, so every trade risks the same fraction of the account regardless of the asset's volatility. It is usually the single biggest driver of the equity curve's shape. See also: r-multiple, atr-stop, max-drawdown ## Slippage The gap between the price you expected and the price you actually got. Real fills are worse than backtest fills, especially on large orders or thin markets. TextToQuant applies slippage against the trade direction so a backtest never assumes a better price than a live order would get. See also: take-profit, max-drawdown ## Multi timeframe (MTF) Using a higher timeframe's trend to filter a lower timeframe's signals. For example, only take 1 hour longs when the daily trend is up. TextToQuant reads only the last completed higher timeframe bar, so a multi timeframe filter never peeks at data from the future. See also: moving-average, rsi ## Sharpe ratio Return per unit of total volatility: a risk adjusted return. Higher is better: it rewards smooth returns and penalizes wild swings. Its blind spot is that it treats big upside moves as 'risk' the same as downside ones. See also: sortino-ratio, deflated-sharpe ## Sortino ratio Like Sharpe, but it only penalizes downside volatility. Useful when you don't want to punish a strategy for large upside moves. It measures return relative to the risk of losing, not the risk of winning big. See also: sharpe-ratio ## Maximum drawdown (MDD) The largest peak to trough drop in the equity curve. The pain metric: the worst losing streak you'd have had to sit through. A strategy you can't hold through its drawdown is one you won't actually earn, no matter how good the final number looks. See also: monte-carlo, position-sizing ## Breakout Entering when price closes beyond a recent high or low. A close above the previous 20 days' high says price just made a one month high: a simple momentum entry. The engine reads 'breakout' three ways (the N day high, a Donchian band, or a confirmed swing high), so it stops and asks you to pick rather than guessing. Breakouts pair naturally with trailing stops to let the resulting trend run. See also: moving-average, trailing-stop ## Trailing stop A stop that ratchets in the trade's favor, locking in open gains. A 10% trailing stop exits if price falls 10% from its highest point since entry, letting winners run while capping how much profit you give back. See also: take-profit, atr-stop ## Take profit (target) A price at which the trade closes in profit. Often set as a multiple of the risk: a 3:1 target risks one unit to make three. Fixed targets cap upside but raise win rate; trailing exits do the opposite. See also: r-multiple, trailing-stop ## Sequential entry Entry conditions that must happen in order, not all on the same bar. Written with 'then' (crosses above the 21 EMA, then retests it), a sequential entry fires only when each step occurs after the previous one. Each step has a timeout: if the next condition doesn't arrive within a set number of bars (50 by default, or 'within N bars'), the sequence resets. It models setups that unfold over time (a signal followed by a confirmation), where plain AND would miss the ordering. See also: moving-average, breakout ## Portfolio (book) Several assets traded from one shared pool of capital, each with its own strategy. Also called the 'book'. Unlike a single backtest, where one symbol has its own balance, a portfolio is a roster of assets competing for one capital pool. It has a shared equity curve, per asset attribution, and a skip ledger of signals that could not be funded. A contention rule decides who gets the cash when assets signal together. See also: contention, position-sizing, max-drawdown ## Contention The rule that decides which asset gets the cash when several signal at once. In a shared capital portfolio, two assets can want to enter on the same bar with only enough cash for one. Contention resolves it: 'rank' lets priority order win, 'prorata' splits the cash proportionally, and 'strength' fills the strongest signal first. Any signal left unfunded is logged in the skip ledger, not dropped silently. See also: portfolio, position-sizing ## Deployment A backtested strategy running forward on live market data, in paper, signal only or live mode. A deployment binds one strategy to a market, a timeframe, a sizing rule and a set of risk limits, and evaluates it on every new bar. In paper mode it simulates fills, in signal only mode it sends alerts, and in live mode it places real orders on a connected exchange account. It makes the same entry and exit decisions as the backtest on the same candles. See also: paper-trading, kill-switch, position-sizing ## Paper trading Running a strategy on live prices with simulated money and simulated fills. Paper trading uses the same engine and risk checks as live trading, but fills orders against the market itself, with modelled slippage, fees, funding and liquidation. It is accurate about a strategy's decisions and usually slightly optimistic about fills, because it cannot model order book depth, queue position or exchange rejections. See also: deployment, slippage ## Kill switch An emergency control that blocks every new order that adds risk, at once. Activating a kill switch blocks strategy entries, copied entries and risk adding manual orders, and cancels the working orders of the strategy deployments it covers. It does not close positions: open positions keep their stops, and closing orders still work. To be flat, activate the kill switch first, then close positions. See also: deployment, max-drawdown ## Leverage Trading a position larger than the margin that backs it. At 10× leverage, $1,000 of margin controls a $10,000 position, so a 1% price move changes your equity by 1% of $10,000, or 10% of the margin. Leverage multiplies gains and losses alike, and it brings the liquidation price closer to the entry. See also: liquidation, position-sizing ## Liquidation The forced close of a leveraged position when its margin can no longer cover the loss. Each leveraged futures position has a liquidation price, roughly entry × (1 − 1/leverage + maintenance margin) for a long. If the mark price reaches it, the exchange closes the position and the margin is lost. A stop loss well inside the liquidation price is what keeps a loss bounded. See also: leverage, atr-stop ## Funding rate The periodic payment between longs and shorts that keeps a perpetual future near the spot price. Perpetual futures never expire, so exchanges charge funding, typically every eight hours. When funding is positive, longs pay shorts; when negative, shorts pay longs. On TextToQuant funding paid or received is folded into a trade's realized P&L when it closes. See also: slippage, leverage ## Reduce only An order flag that lets the order shrink or close a position, never open or grow one. A reduce only order can only reduce an existing position. If there is nothing to reduce, or the order would flip the position to the other side, the exchange cancels it. On futures, platform closes and protective stops are sent reduce only, so they can never accidentally open a new position. Spot orders have no reduce only flag. See also: take-profit, trailing-stop ## High water mark The highest value an account or a profit total has reached, used as the reference for drawdown and fees. A drawdown is measured from the high water mark, so a breaker can trip when equity falls a set percentage below it. In copy trading, a leader's profit share is charged only on profit above the follower's high water mark, so a loss must be earned back before any new fee is due. See also: max-drawdown, copy-trading ## Copy trading Mirroring another trader's live strategy on your own account, sized to your own budget. A follower sets an allocation and limits, and every trade the leader's strategy makes is placed on the follower's own exchange account, scaled to that allocation and bounded by the follower's caps. Trading capital never leaves the follower's own account, the only money that reaches the leader is a profit share on new profit, and the follower can pause or stop at any time. See also: high-water-mark, position-sizing