# BDTLab Trading API · Canvas reference (v0.2)

BDTLab code looks like Python, but it is **not general Python**. The source is parsed as data into a deterministic decision graph. It is never executed as Python. No imports, network, file access, loops, arbitrary calls, or live execution; there are no live orders. Check validates the source and graph; Run passes that graph to the same historical paper Backtester used by the Tests workspace.

Start by copying a complete example below into Canvas. Change the named values near the top, press Check code, then Run test. The reference search matches function names, settings, data fields, and limits; click a topic on the left to see its explanation.

## Quick start

This example is a paper rule, not a recommendation. The reaction is the completed SPY candle from 2:30–2:31 Chicago time. The option entry is separately scheduled for 2:41. The numbers near the top are the tuning controls.

```python
strategy(name="Green SPY call")
TARGET_PCT = 50
CONTRACTS = 1
reaction = candle("SPY", timeframe="1m", start="14:30", evaluate_at="14:31", timezone="America/Chicago")
if reaction.color == "green":
    buy_option("SPXW", side="call", entry="14:41", cutoff="14:42", deadline="14:50", timezone="America/Chicago", contracts=CONTRACTS, target_pct=TARGET_PCT)
else:
    skip()
```

The engine selects an observed same-day nearest-ATM contract, models an entry at observed ask plus configured slippage, and an exit at observed bid less slippage and fees. A simulated trade is not an independently verified live fill. On a day without adequate timestamped candles, contract, spot, or executable quotes, the result is unavailable rather than made up.

## strategy(name)

The first statement identifies the strategy. The name appears in your private library and test results; it does not affect trading decisions. Use `strategy(name="Readable description")` once, at the top. The name must be nonempty and at most 80 characters. A strategy needs at least one completed candle read and a terminal action on each branch that should trade; an omitted false branch is an implicit no-trade.

## Named settings

Put adjustable literals just below `strategy`: `TARGET_PCT = 50`, `MIN_MOVE_PCT = -0.5`, `CONTRACTS = 1`, `START_TIME = "14:30"`. Use a setting name in `candle(...)`, `buy_option(...)`, or the right side of an `if` comparison. These names are substituted at Check time as fixed values, not interpreted as mutable Python state. Change a number and Check again to run the new graph. Names are unique, up to 32 alphanumeric/underscore characters and begin with a letter. A name cannot shadow `strategy`, `candle`, `buy_option`, or `skip`. Values are finite numbers, short strings, booleans, or `None`; each API setting still has its own range and type checks. No expressions in the top settings block: use a plain literal or a signed number.

## candle(symbol, timeframe, start, evaluate_at, timezone)

`reaction = candle("SPY", timeframe="1m", start="14:30", evaluate_at="14:31", timezone="America/Chicago")` reads one specific completed underlying candle. `symbol` is an underlying (not an option); `timeframe` can be `"1m"`, `"5m"`, `"15m"`, `"30m"`, or `"60m"`. `start` is that candle's market-local HH:MM start; `evaluate_at` must be at or after its completion. Use `"America/New_York"` or `"America/Chicago"`, with daylight saving handled by the replay clock. The variable is frozen at its completed value, not continuously updated. Multiple named candle reads can precede a decision. To backtest other tickers, local full-tape underlying and option coverage must actually be present; the legacy source has narrower SPY/SPXW support.

A completed bar is evidence at/after its completion, never at its start. Reading the third candle's open via `candle(...)` does NOT permit an entry at that candle's opening time: its high/low/close would be future information until it ends. For an exact C3-open entry rule, the current code language cannot use C3's still-forming candle as evidence. Do not move `evaluate_at` backward to fake it.

## candle fields

After `reaction = candle(...)`, use `reaction.open`, `.high`, `.low`, `.close` (underlying price), `.volume` (underlying shares), `.body` (`close-open`, signed price units), `.range` (`high-low`, price units), `.upper_wick` (`high-max(open,close)`), `.lower_wick` (`min(open,close)-low`), `.body_pct` (`abs(close-open)/open * 100`), `.range_pct` (`(high-low)/open * 100`), `.color` (`"green"`, `"red"`, or `"flat"`), and `.day_of_week` (`"Monday"` through `"Sunday"`). The percent fields are percentage points: compare `body_pct > 0.5` for more than half a percent, not `> 50`. Price/range/wick fields use underlying units, not option premium.

High, low, range, wick lengths, range percentage, and volume require valid completed underlying OHLCV in the selected historical source. Missing historical volume is unavailable, never zero; missing high/low are never inferred from open/close. `body` and `body_pct` only need valid open and close. `day_of_week` derives from the session date. This is not relative volume (RV): there is no implicit average-volume baseline or same-window RV>1 filter.

## if comparison

Use one comparison per `if`: `if reaction.close > reaction.open:` or `if move_pct >= MIN_MOVE_PCT:`. Operators: `==`, `!=`, `>`, `>=`, `<`, `<=`. The left is a completed candle field or a derived numeric name. The right is a literal, named setting, field, derived value, or arithmetic expression. Numeric order comparisons require numbers; colors/day names use equality or inequality. Nested `if` blocks express multiple requirements. An omitted `else` on a condition means no trade on its false path; writing `else: skip()` makes that intent obvious when editing. The source compiler rejects boolean `and`/`or` and chained comparisons rather than guessing their execution order.

## Derived numeric values

After a candle read, name a calculation: `width = reaction.high - reaction.low`, then compare it later with `if width > 1.0:`. `+`, `-`, `*`, `/` and parentheses operate on completed candle numbers, named settings, and earlier derived numbers. Derived numbers are part of the graph and use the same frozen observations, not executed Python. A value cannot refer to a future/unread candle or a variable missing on an incoming branch. Division by zero, overflow, missing fields, and nonfinite results make the day unavailable; they never silently become a trade. Reassigning a name or doing arbitrary Python assignment is rejected.

## abs(value)

Absolute magnitude of a number. Example: `size = abs(reaction.close - reaction.open)`. This is a safe built-in expression operator in the graph, not a call into user Python. Exactly one numeric argument.

## min(a, b)

The smaller of two numeric values. Example: `body_bottom = min(reaction.open, reaction.close)`. Exactly two numeric arguments. Only completed observations are permitted.

## max(a, b)

The larger of two numeric values. Example: `body_top = max(reaction.open, reaction.close)`. Exactly two numeric arguments.

## pct_change(current, baseline)

Percentage change as `(current - baseline) / baseline * 100`. Example: `move_pct = pct_change(reaction.close, reaction.open)`. Exactly two numeric arguments; the baseline must be positive. This is price change, not relative volume or option return.

## clamp(value, lower, upper)

Bound a number to the inclusive `lower`/`upper` range. Example: `threshold = clamp(reaction.body_pct, 0, 2)`. Exactly three numeric arguments; lower may not exceed upper.

## buy_option(symbol, side, entry, cutoff, deadline, timezone)

Terminal paper buy of one long option position per path. Its knobs are deliberately explicit:

- `symbol` is the option underlying, for example `"SPXW"`; `side` is `"call"` or `"put"`.
- `entry`, optional `cutoff`, and `deadline` are HH:MM exchange-local clocks in `timezone` (`"America/Chicago"` or `"America/New_York"`). Require `entry <= cutoff < deadline`; omitted cutoff defaults to entry plus one minute. A decision candle must be completed before entry.
- `contracts` is 1–100 whole contracts. If omitted, the runner's quantity default applies.
- `target_pct` is a 10–200% profit target, `stop_loss_pct` a 5–95% loss limit, and `trailing_stop_pct` a 5–95% fall from the peak observed bid. Each is optional; omission disables that risk rule in this code graph. `trailing_activation_pct` is an optional 0–200% gain threshold, meaningful with a trailing stop: the trail begins only after an observed post-entry bid reaches the threshold, and can first exit on a later observed quote. Without an activation setting, the trail is active from entry. These percentages use the modeled option entry or observed option bid peak, not the underlying's price.
- `max_spread_pct` is 0–100%; an entry is filtered when `(ask-bid)/ask * 100` exceeds it. Zero accepts only a zero spread.

At each valid observed bid, stop, trailing stop, then target are checked before the deadline. These knobs tune paper behavior; they do not create unseen quote observations. A missing executable bid at exit makes the day unavailable. The engine uses one observed same-day nearest-ATM option. Custom strike offsets, limit fills, short positions, and live orders are not supported by this code language.

## skip()

Terminal no-trade decision for the selected path. It takes no arguments. Use `else: skip()` to make a no-trade branch explicit and easy to edit; omitting the false branch of an `if` also results in no trade. A no-trade is different from an unavailable day: no-trade means the available evidence did not meet your rule.

## Worked example: completed range filter

Here the 2:30–2:31 completed SPY bar determines the range zone; only after the next 1-minute bar completes at 2:32 does the code read its open. This deliberately does not claim an entry at 2:31 when the next bar began. Change `ZONE_FRACTION`, `MIN_BODY_PCT`, `TARGET_PCT`, and `TRAIL_START_PCT`, then Check again.

```python
strategy(name="Completed range confirmation")
ZONE_FRACTION = 0.66
MIN_BODY_PCT = 0.1
TARGET_PCT = 50
TRAIL_START_PCT = 40
first = candle("SPY", timeframe="1m", start="14:30", evaluate_at="14:31", timezone="America/Chicago")
second = candle("SPY", timeframe="1m", start="14:31", evaluate_at="14:32", timezone="America/Chicago")
upper_zone = first.low + first.range * ZONE_FRACTION
if first.body_pct >= MIN_BODY_PCT:
    if second.open > upper_zone:
        buy_option("SPXW", side="call", entry="14:32", cutoff="14:33", deadline="14:50", timezone="America/Chicago", contracts=1, target_pct=TARGET_PCT, stop_loss_pct=30, trailing_stop_pct=20, trailing_activation_pct=TRAIL_START_PCT, max_spread_pct=25)
    else:
        skip()
else:
    skip()
```

## Historical evidence and Backtester

Canvas Check parses source into the same version-2 graph that the Backtester validates. Check does not download market data or prove that every selected day has evidence. Canvas Run carries the checked graph to Single Test; saved code can be selected in Batch Test. The queued replay checks actual local data when it runs. Data might exist for one symbol and date but not another. Completed underlying bars are read from historical sources, with timestamp and availability checks; option decisions require a contemporaneous underlying spot, observed same-day ATM contract, and executable ask/bid evidence. If a date is partial or unavailable, inspect the per-date status and trace rather than treating the numeric total as complete-range performance. Simulations include configured fees/slippage, not independently validated real-world fills.

## Limits and unsupported requests

A code source is AST-parsed with a structural 1,024-node complexity guard and lowers to at most 64 graph nodes; there is no arbitrary `.py` byte cap. Every branch must be deterministic and supported by evidence available at that simulated time. No dynamic calls, imports, classes, loops, list comprehensions, arbitrary Python `def`, API requests, secret access, short options, custom strike offsets, limit fills, partial exits, continuously forming candles, post-entry reads, rolling indicators, or guaranteed GEX/relative-volume feed in this code language. A feature visible in another JSON graph or the raw lake is not automatically a Canvas function. Unsupported features fail validation; no proxy data is invented. If you need one, describe its data source, time availability, and exact rule before adding it to the engine.

## AI handoff

The AI Assist tab downloads your prompt plus this exact reference. Ask an outside AI to return only a `.py` strategy using the functions listed here; upload it to Canvas, inspect the editable numbers and clocks, and Check before Run or Save. Nothing is sent to an outside AI from the site. Do not put credentials, API tokens, or live-order instructions in your prompt.
