Signal DSL Reference
Python-based scripting language for backtest and live strategy execution.
This page focuses on DSL grammar and execution model.
Getting Started
Use this reference to author the strategy itself, then validate and execute it through the caller's authoritative surface:
- Choose the strategy type and read its current input contract.
- Write the script with the ticker, data, operator, and sizing rules below.
- Submit the requested backtest or live-strategy action through that surface.
- Inspect its validation outcome before treating the strategy as running; correct any returned diagnostic and resubmit only when appropriate.
Browser application agents must discover those operations from the application command catalog. Public API clients use the public API catalog and current OpenAPI contract. This DSL reference does not duplicate either execution schema or link one caller surface into the other.
1) Ticker Grammar
This section defines the standard exchange-traded spot and perpetual ticker format. Read-only data sources and Polymarket prediction markets have their own identifiers, documented below; obtain exact market identifiers from discovery instead of reconstructing them from this pattern.
Rule: EXCHANGE[.namespace]:BASE/QUOTE[:SETTLE]
| Format | Market Type | Example |
|---|---|---|
EXCHANGE:BASE/QUOTE | Spot | BINANCE:BTC/USDT |
EXCHANGE:BASE/QUOTE:SETTLE | Perpetual | BINANCEFUTURES:BTC/USDT:USDT |
EXCHANGE.namespace:BASE/QUOTE:SETTLE | Namespaced perpetual | HYPERLIQUID.xyz:AAPL/USDC:USDC |
:SETTLEsuffix determines perpetual vs spot.- Namespace, when present, is always lowercase and joined to the exchange with a dot (
.). - Every traded market referenced in script logic must be included in
universe. Read-only reference keys such asCHAINLINK:BTC/USDare declared in an annotated lane'sreads, not added to the trading universe. - Binance Futures public examples use
BINANCEFUTURES:BASE/QUOTE:SETTLE. Concrete data/chart prefixesBINANCEFUTURESUSD:BASE/QUOTE:SETTLEandBINANCEFUTURESCOIN:BASE/QUOTE:SETTLEare also accepted when supplied by a chart, API, or user; do not treat them as invalid.
Hyperliquid Namespace Rules
Hyperliquid supports multiple perp universes (default + perpDex namespaces).
- Global scope:
HYPERLIQUID:*(root/default + namespaced markets mixed) - Namespace scope:
HYPERLIQUID.<ns>:*(only that namespace; lowercase ns) - Examples:
HYPERLIQUID:BTC/USDC:USDCHYPERLIQUID.xyz:AAPL/USDC:USDCHYPERLIQUID.km:AAPL/USDH:USDH
Always discover canonical symbols through the caller's current Exchange capability surface. Browser application agents use the command catalog; public API clients use the Exchanges API.
2) Supported Exchange IDs
Strategy-executable venues
| Exchange ID | Type | Backtest | Paper | Trade |
|---|---|---|---|---|
binance | Spot | Yes | Yes | Yes |
binancefutures | Perpetual | Yes | Yes | Yes |
hyperliquid | Perpetual (supports .namespace) | Yes | Yes | Yes |
lighter | Perpetual | Yes | Yes | Yes |
extended | Perpetual | Yes | Yes | Yes |
polymarketperp | Perpetual | No | Yes | Yes |
polymarket | Prediction market | Yes | Yes | Yes |
This is the current strategy-mode eligibility, not a promise that every feed,
timeframe, or order type is available for every venue. Check the selected
venue's capability contract and the exact market before execution. Polymarket
prediction-market scripts may use a readable POLYMARKET:<market_slug> or a
canonical outcome ticker returned by market discovery; neither should be
inferred from the standard spot/perpetual pattern.
Reserved (Read-Only Data)
| Exchange ID | Type | Example |
|---|---|---|
fx | Forex | FX:EUR/USD |
index | Indices | INDEX:^IXIC |
yahoo | Multi-asset data source | YAHOO:AAPL |
kiskr | KR stock data | KISKR:005930 |
kisus | US stock data | KISUS:AAPL |
3) Execution Modes
| Mode | Behavior | Access Pattern |
|---|---|---|
isolated (default) | Run same logic per ticker | Use auto-injected series (close, high, ...) |
cross | Evaluate all tickers together | Use get_data(ticker, timeframe?) |
- Isolated context key:
context["ticker"](full ticker) - Cross context key:
context["tickers"](full ticker list) - Full ticker format is always required in
universe,get_data(...), and literal assets passed toentry(...),exit_position(...),adjust(...).
Event-scoped evaluation with @on_market
Large tick strategies can declare independent evaluation lanes. A normal live tick update executes only lanes whose marker contains the updated market; the resulting Actions still use the ordinary SignalPolicy, FanOut, and Broker path.
POLY_LIT = 'POLYMARKETPERP:LIT/PUSD:PUSD'
LIGHTER_LIT = 'LIGHTER:LIT/USDC:USDC'
@on_market(POLY_LIT, LIGHTER_LIT, primary=POLY_LIT)
def lit_lane():
poly = get_data(POLY_LIT)
lighter = get_data(LIGHTER_LIT)
# emit(...) as usual
- Positional tickers are wake markets.
- Ordered ticker collections must use tuple/list; set is rejected because process hash seeds can change its order.
reads=[...]declares dynamicget_data()markets that cannot be inferred statically.trades=[...]declares dynamic order targets such asrebalance_to()universes.tradesdoes not grantget_data()access; a market used by both must also be declared inreads.primary=...anchors the data fields available for that cadence andcontext['ticker']; it defaults to the first wake market. Candle lanes can use bareclose, while tick lanes use scalarpriceand do not receive bare OHLCV.- Traded wake/read/trade markets must be present in
universe. Read-only reference keys such as Chainlink are declared inreadsbut do not belong in the trading universe. get_data(ticker, timeframe='1m')remains valid inside a tick lane.- At candle and backtest boundaries, all lanes execute once in source order.
- Annotated bodies are explicit cross-market lanes;
mode='isolated'does not add an implicit per-ticker loop around them. - Scripts without markers retain the existing isolated/cross execution behavior.
4) Timeframes
- Candle:
1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M - Tick:
tick(live only; backtest does not execute tick mode) - Exchange-specific support and range limits are enforced when the request is validated. Do not reject a timeframe from this generic DSL page alone; validate it against the requested exchange.
Tick mode notes:
get_data(ticker).pricereads that ticker's current evaluation price.- For candle indicators, explicitly select a timeframe:
get_data(ticker, '4h').close. - A default tick view has no OHLCV candles. Tick prices are never substituted for candle closes.
- A tick strategy has no default candle, so bare
bar_indexis not available. Count bars on an explicit candle instead, e.g.len(get_data(ticker, '1m').time_close).
5) Built-in Variables
| Category | Variables |
|---|---|
| OHLCV series | open, high, low, close, volume |
| Time series | time, time_close, hour, minute, weekday |
| Tick mode | price |
| Context | context["ticker"], context["tickers"] |
| Position state | position, entry_count, avg_price, position_opened_at, unrealized_pnl, current_price, funding_rate |
| Account | balance |
| Contract prices | mark_price, index_price |
balance is the free balance (the total when the venue reports no free amount)
of the evaluated ticker's quote or settlement asset, in the account that trades
the ticker: USDT for BINANCEFUTURES:BTC/USDT:USDT, BTC for BINANCE:ETH/BTC.
Use free_balance(currency) for another asset. mark_price and index_price
are the venue's latest contract prices, 0.0 when the venue publishes none.
time and time_close are Unix epoch milliseconds. For an epoch-aligned
five-minute round, (time % 300_000) >= 270_000 selects its final 30 seconds.
Use the published schedule for markets whose boundaries are not epoch-aligned.
position_opened_at is the current strategy position's opening fill time in UTC
milliseconds, scoped like position and avg_price. Adding to or partially
reducing inventory retains it. Closing inventory clears it; reversing direction
starts a new episode. It is None while flat or when the execution source cannot
establish the original fill time. A receipt, signal, or recovery time is never
substituted. Broker state hydration preserves this value across Runner restarts.
get_entry_price(ticker) reads the current strategy position's actual average
entry price from the accepted account snapshot. It is the ticker-selected form
of avg_price; it is not the current market price or an unfilled order's future
price. It returns None while flat or when the entry basis is unknown. It uses
the same account binding as get_position(ticker) and performs no network read.
All arithmetic remains ordinary strategy code:
p = get_entry_price(ticker)
if p is not None:
target = p + 2 * (p - initial_stop) # initial_stop belongs to the strategy
Count confirmed candles using their existing close-time series; no persistent counter is needed. This example closes after 24 completed bars of the strategy's evaluation timeframe. A candle ending exactly at entry does not count.
if position != 0 and position_opened_at is not None and len(time_close) >= 24:
emit(time_close[-24] > position_opened_at, exit_position(context['ticker']))
Indexing time or time_close declares history demand just like indexing
close. For a secondary timeframe, use the corresponding get_data() series.
weekday follows ISO numbering: Monday=1 through Sunday=7. Use weekday, hour, and minute for calendar schedules such as "every Monday" or "daily first candle"; do not approximate named weekdays with bar-count modulo logic.
Persistent Strategy State
Use the state namespace when a strategy must retain a value between evaluations.
| Function | Description |
|---|---|
state.get(name, default=None) | Read bar-persistent state, initializing it once from default. |
state.set(name, value) | Update persistent state and return the stored value. |
state.intrabar(name, default=None) | Read state that also persists across realtime updates within an unconfirmed bar. |
active_factor = state.get("active_factor", default="resilient_growth")
if funding_regime:
active_factor = state.set("active_factor", "funding")
Normal state.get values follow confirmed-bar semantics: an unconfirmed realtime
update starts from the last committed value. state.intrabar retains updates
within the current realtime bar. Pine-transpiled strategies use the same state
store automatically; HeyTraders DSL should use state.*.
Trade History
history is this strategy's own record of what it actually traded: the fills
and settlements its exchange reported, in every mode (Backtest, Paper, Live).
Use it instead of hand-maintained state counters when a decision depends on
past trades.
| Query | One row is | Typical use |
|---|---|---|
history.fills(...) | One venue fill. Two partial fills of one order are two rows. | Prices, sizes, fees of recent executions |
history.entries(...) | One order that opened or added exposure (all its fills). | Limit the number of entries per period |
history.trades(...) | One completed trade: flat → held → flat. Adds and partial exits belong to it; a reversal closes it and starts the next. | Previous trade result, loss streaks |
history.settlements(...) | One prediction-market resolution of a token this strategy held. | Previous round's outcome |
Every query takes exactly one of last=N or window='30m' (units ms, s,
m, h, d), plus optional ticker= and account= filters. Filters apply
before last. Arguments must be literals or prepared configuration constants,
so the runtime knows how much history to keep. Results are oldest first and
support len(), .count(), indexing, iteration and .last() (None when
empty). history.trades(...).loss_streak() counts consecutive losing trades,
newest first, within that selection.
signal = close > close[1]
# At most three entries per hour.
recent_entries = history.entries(window='1h')
# Halve the size after a losing trade.
previous = history.trades(last=1).last()
size = 0.01 if previous is None or previous.realized_pnl >= 0 else 0.005
emit(signal & (recent_entries.count() < 3),
entry(context['ticker'], 'LONG', Quantity(size), when=NoPosition()))
# Previous prediction-market round.
last_round = history.settlements(last=1).last()
won_last_round = last_round is not None and last_round.outcome == 'win'
Row fields:
| Query | Fields |
|---|---|
| fills | id, order_id, label, account_id, exchange, ticker, side, quantity, price, fee, fee_asset, timestamp, status (pending/confirmed/failed), pending, position_effect, realized_pnl |
| entries | order_id (also id), label, account_id, ticker, side, quantity, timestamp, pending |
| trades | round_trip_id (also id), account_id, ticker, side (LONG/SHORT), quantity, closed_quantity, opened_at, closed_at (also timestamp), realized_pnl, pnl_asset, close_reason (EXIT/FLIP/SETTLEMENT), fills (number of fills, int) |
| settlements | id, account_id, exchange, ticker, quantity, payout, outcome (win/loss), winning_asset_id, realized_pnl, timestamp, status, pending |
label is the label= given to the order that filled, so a strategy can tell
its own order roles apart, for example [f for f in history.fills(last=20) if f.label == 'tp1'].
realized_pnlis the exchange-side value net of a fee paid in the quote currency. A trade'srealized_pnlsums its fills and settlement. When any part is not established, reading it raises an error instead of returning 0.- Polymarket fills are
pendingfrom MATCHED until CONFIRMED, and a failed match disappears. In Live and Paper, positions, and thereforeHasPosition/NoPositionand sells, follow confirmed shares only; a pending buy already counts inhistory.entries. Backtest fills are confirmed when matched. - After a Live/Paper restart the history is restored from the exchange record. Until it is restored, reading it raises an error instead of looking empty.
- A history view is valid only during the evaluation that created it. Query again in the next evaluation.
6) Core Functions
Data Access
| Function | Signature | Notes |
|---|---|---|
get_data | get_data(ticker: str, timeframe: str = current) | Required in cross mode for external series access |
align_timeframe | align_timeframe(remote_data, result, *, timeframe, base_time=time) | Merge a result calculated on native remote candles onto the current timeline after each remote candle closes |
get_position | get_position(ticker: str) -> float | The strategy's signed position in ticker (0.0 when flat). ticker must be a declared trade target (universe, or trades=[...] of the evaluation); any other ticker raises. |
get_entry_price | get_entry_price(ticker: str) -> float | None | Average entry price of that position; None while flat or when the entry basis is unknown. Same target rule as get_position. |
Multivariate numerical models (stats)
stats provides composable numerical operations in ordinary DSL scripts and
@on_market lanes. It uses the same point-in-time evaluator in backtest, Paper,
and Live. It does not change order types or Broker execution. Imports are still
restricted; use these functions instead of importing NumPy or scikit-learn.
Build labelled observations with existing get_data() and explicit trailing
slices. Those slices remain the source of truth for warmup and subscriptions:
columns = {}
for ticker in context['tickers']:
data = get_data(ticker)
columns[ticker] = (data.time_close[-61:], data.close[-61:])
returns = stats.panel(columns).returns()
if len(returns) == 60 and returns.is_finite:
model = stats.pca(returns, 3, standardize=True)
scores = (-model.residuals(returns)).row()
rebalance_to(score_to_weights(scores, gross=1.0), leverage=1)
This is an expression example, not a profitability claim. n_components must
fit the supplied observation/column dimensions. Panel rows are observations;
columns are the named markets. fit_end is the last supplied row timestamp.
PCA always uses its training center/scale on subsequent inputs. A model accepts
the same named columns in a different order and reorders them correctly; a
changed set of markets requires an explicit refit.
| Function | Contract |
|---|---|
stats.panel(columns, join='strict') | {name: (timestamps, values)} → immutable labelled matrix. Strict mode requires identical, strictly increasing timestamps; explicit join='inner' retains shared timestamps. Neither fills missing values. |
stats.matrix(values, columns=None, index=None) | Numeric rows and optional unique column/row labels. A one-dimensional input becomes a column vector. |
stats.pca(x, n_components, standardize=False) | Fitted model with components, center, scale, explained_variance, explained_variance_ratio, fit_end, transform(x) and residuals(x). |
stats.svd(x) | Reduced (u, singular_values, vt). Reconstruct with u @ stats.diag(singular_values) @ vt. |
stats.eigh(x) | Symmetric matrix → (eigenvalues, eigenvectors), descending eigenvalue order. |
stats.diag(values) | One-dimensional sequence → diagonal matrix. |
stats.covariance(x, decay=None, shrinkage=0.0) | Sample covariance (n-1); optional exponential weighting, normalized by 1 - sum(weights**2). Shrinkage λ uses (1-λ)C + λ I trace(C)/N. |
stats.solve(a, b) | Solve a nonsingular square system. A score dictionary keyed by a.index returns a solution dictionary keyed by a.columns. Singular systems raise an error. |
stats.lstsq(x, y, ridge=0.0) | Least-squares coefficients for one/multiple responses; optional nonnegative ridge. No automatic intercept. |
stats.regression(y, x, intercept=True, ridge=0.0) | Multi-response, multi-factor model; coefficients, intercept, fit_end, predict(x), residuals(y, x). The intercept is not ridge-penalized. |
Matrices expose shape, columns, index, is_finite, transpose .T,
to_list(), row(at=-1) (column-label dictionary), column(name=0) (Series),
column_dict(name=0) (row-label dictionary), select(columns), tail(n),
returns(log=False), mean(), std(ddof=1) and drop_missing().
Arithmetic +, -, *, /, **, abs, unary -, and matrix product @ are supported.
mean()/std() reduce the observation axis and retain column labels. A named
column selection uses a string; an integer selects by position.
Missing/nonfinite observations are preserved by the panel and rejected by model
fitting. Apply an explicit missing-data rule when needed. drop_missing() removes
entire rows; if applied before returns(), adjacent remaining rows can span a
larger time interval. Align/sample according to the strategy's intended horizon.
Least squares is available for rank-deficient systems; solve never silently
switches to a pseudo-inverse. Covariance shrinkage is an explicit choice.
Use existing state to fit less often than you evaluate:
columns = {}
for ticker in context['tickers']:
data = get_data(ticker)
columns[ticker] = (data.time_close[-61:], data.close[-61:])
returns = stats.panel(columns).returns()
if len(returns) == 60 and returns.is_finite:
model = state.get('residual_model')
if model is None or bar_index % 42 == 0:
model = stats.pca(returns, 3)
state.set('residual_model', model)
scores = (-model.residuals(returns)).row()
rebalance_to(score_to_weights(scores), leverage=1)
Fitted models/matrices are immutable and reused during existing state-copy
transactions. State keeps its existing confirmed-bar/intrabar semantics and is
local to the evaluation scope; separate lanes do not share a model implicitly.
On a new policy instance the example refits from its declared history; it does
not promise to restore a pre-restart model version. The existing Pine matrix
namespace is unchanged and remains distinct from stats.matrix.
Covariance-adjusted allocation can use the same observations and order helper:
columns = {}
for ticker in context['tickers']:
data = get_data(ticker)
columns[ticker] = (data.time_close[-61:], data.close[-61:])
returns = stats.panel(columns).returns()
if len(returns) == 60 and returns.is_finite:
scores = cs_demean(returns.mean().row())
risk = stats.covariance(returns, decay=0.97, shrinkage=0.2)
target = cs_demean(stats.solve(risk, scores))
rebalance_to(normalize_gross(target, gross=1.0), leverage=1)
Dollar neutrality above is not PCA-factor neutrality. To project desired positions off factor loadings and a constant exposure, use multivariate regression with an intercept. Gross normalization preserves the projection; later caps, lot rounding and partial fills may change realized neutrality.
columns = {}
for ticker in context['tickers']:
data = get_data(ticker)
columns[ticker] = (data.time_close[-61:], data.close[-61:])
returns = stats.panel(columns).returns()
if len(returns) == 60 and returns.is_finite:
model = stats.pca(returns, 3)
scores = (-model.residuals(returns)).row()
desired_rows = []
for ticker in returns.columns:
desired_rows.append([scores[ticker]])
desired = stats.matrix(desired_rows, index=returns.columns)
loadings = model.components.T
exposure = stats.regression(desired, loadings, intercept=True)
neutral = exposure.residuals(desired, loadings).column_dict()
rebalance_to(normalize_gross(neutral, gross=1.0), leverage=1)
Readable Polymarket markets (Live / Paper)
For a binary prediction market, use POLYMARKET:<market_slug> from the market
search result's strategy_ticker. A market slug identifies one binary
question; an event slug may contain several markets and cannot select one for
you. Keep the exact lowercase slug. No numeric token ID or manual binding JSON
is needed. The following placeholder must be replaced with a real search result:
M = 'POLYMARKET:<market_slug>'
@on_market(M, reads=[M], trades=[M])
def quote():
yes_book = get_data(M).book
no_book = get_data(M, outcome='NO').book
if yes_book is not None and no_book is not None:
emit(True, entry(M, 'BUY', Quantity(5), outcome='YES',
execution='PASSIVE', limit_price=0.20,
when=NoPosition(), label='yes-entry'))
emit(True, entry(M, 'BUY', Quantity(5), outcome='NO',
execution='PASSIVE', limit_price=0.20,
when=NoPosition(), label='no-entry'))
entry,exit_position, andadjustrequireoutcomefor a market slug.limit_priceand quantity refer to that selected outcome. No at 0.20 means buying No tokens at 20 cents, not submitting a Yes sell at 0.80.get_data(M)defaults to Yes data (Up for an Up/Down market).get_data(M, outcome='NO')reads No's actual data and book. Candle data accepts the same selector, e.g.get_data(M, '1m', outcome='NO').close.- Outcome names are case-insensitive exact catalogue labels; they can also be
chosen by an expression, e.g.
outcome=('YES' if condition else 'NO'). The market argument must be a literal or immutable top-level constant. @on_market(M)wakes on either outcome.reads=[M]permits reading both;trades=[M]permits ordering either. Explicit condition assets also select an outcome:HasPosition(asset=M, outcome='NO', side='LONG'). A condition with no asset, such asNoPosition(), uses the order's selected outcome.- Include the readable market once in
parameters.universe. It must identify one exact market. A fixed market must be accepting orders when prepared; recurring strategies validate the current round instead of the selected date. - Crypto Up/Down periods up to one day and daily highest/lowest temperature
markets automatically follow subsequent rounds at new strategy registration.
Other readable market slugs stay fixed. Market-slug and outcome syntax is
identical for both; registration resolves the selectors and generates logical
bindings internally. Token IDs, logical aliases and manual binding declarations
are not required from the caller. The catalog's
strategy_recurrencereports this same default; it is information, not a required registration input. The selected round identifies the family even if it closes before registration; admission checks the current round separately. Canonical outcome tickers are still supported by existing strategies. - Polymarket historical backtests remain unsupported. This syntax is for Live and Paper using current books or forward-built candles.
Chainlink stream data (Live / Paper)
Chainlink is a read-only reference source, independent of the traded market.
Use get_data('CHAINLINK:BTC/USD') for the raw stream, or append :TWAP30
or :TWAP60 for the corresponding upstream TWAP stream. BTC, ETH, SOL and XRP
against USD are supported. These keys do not belong in the trading universe.
In an annotated lane, declare the key in reads; market updates trigger the lane.
This example uses timeframe='tick' and a 15-minute BTC market's strategy_ticker.
The placeholder denotes its exact market slug. Registration automatically binds
this supported recurring family to subsequent rounds:
M = 'POLYMARKET:<market_slug>'
FEED = 'CHAINLINK:BTC/USD:TWAP60'
@on_market(M, reads=[M, FEED], trades=[M])
def compare_target():
feed = get_data(FEED)
now_ms = int(time[-1])
round_start_ms = (now_ms // 900_000) * 900_000
target = feed.price_at(round_start_ms) if feed is not None else None
# The feed must match the market's resolution source: raw, TWAP30 or TWAP60.
if target is not None and 0 <= now_ms - feed.timestamp <= 10_000:
emit(feed.price > target,
entry(M, 'BUY', Quantity(5), outcome='Up', when=NoPosition()))
feed.price is the latest source price, feed.timestamp is its source time,
and feed.received_at is the receipt time of the latest snapshot change (all
times are epoch milliseconds). feed.price_at(start_ms) returns the exact
source observation at that boundary, or None when it was not observed.
It never selects a nearby tick, another feed, or the first price after startup.
Pass the desired round's start explicitly so a previous round's target cannot
be reused accidentally. Supported lookup timestamps are multiples of five
minutes; at most 49 boundary points from the latest four hours are retained.
Late exact-boundary observations can fill missing targets without moving the
current price backward. The first value at a boundary is retained.
Publisher shares the existing RTDS WebSocket transport with all consumers,
with at most 12 distinct pair/source subscriptions. Subscriber writes a separate
SHM snapshot for each source; Runner reads it locally. A Publisher restart needs
new stream observations to establish boundaries; missing history remains None.
The stream observations are not an official-target verification response.
These forward snapshots do not add historical backtest support or reference candles.
Two-outcome Polymarket stop entries (Live / Paper)
Polymarket STOP BUY triggers a market buy funded in USDC. Use Notional(...)
for that quote budget, not Quantity(...). For a share-denominated buy use
STOP_LIMIT with both trigger_price and limit_price. SELL stop orders remain
share-denominated. This contract is identical in Paper and Live.
Use each chart_sources entry from market discovery as an exact
outcome-to-ticker pair. The following IDs are valid-shaped examples only;
replace both tickers with the selected market's returned values.
UP = 'POLYMARKET:12345678901234567890/USDC'
DOWN = 'POLYMARKET:22345678901234567890/USDC'
@on_market(UP, DOWN, reads=[UP, DOWN], trades=[UP, DOWN], primary=UP)
def final_window_orders():
up_price = get_data(UP).price
down_price = get_data(DOWN).price
final_30_seconds = (time % 300_000) >= 270_000
both_in_range = (
(up_price >= 0.40)
& (up_price <= 0.60)
& (down_price >= 0.40)
& (down_price <= 0.60)
)
emit(
final_30_seconds & both_in_range & (get_open_order('up-stop-75') is None),
entry(
UP,
'LONG',
Notional(10),
execution='STOP',
trigger_price=0.75,
when=NoPosition(asset=UP),
label='up-stop-75',
),
)
emit(
final_30_seconds & both_in_range & (get_open_order('down-stop-75') is None),
entry(
DOWN,
'LONG',
Notional(10),
execution='STOP',
trigger_price=0.75,
when=NoPosition(asset=DOWN),
label='down-stop-75',
),
)
The two labels and open-order checks keep the pending orders independent and prevent the tick lane from submitting duplicates while an order remains open.
X posts (Live / Paper)
get_data('X:ELONMUSK') returns the account's counted posts as a polars
DataFrame, one row per post, oldest first, like a candle frame:
| column | type | meaning |
|---|---|---|
created_at | Datetime('ms', 'UTC') | post creation time |
status | str | 'pending' (inside the 5-minute deletion grace) or 'confirmed' |
A post deleted within the grace is removed, so the row count over a period is
the Polymarket count (posts, quotes, reposts and self-thread replies). Rows
cover the retained history (up to 90 days; collection began 2026-10-01). The
value is None until the complete history is loaded or while the collector is
disconnected; never a partial table. Only continuously collected accounts
(X_POSTS_ALWAYS_ON, currently ELONMUSK) have a key.
POSTS = 'X:ELONMUSK'
START, END = 1791547200, 1792152000 # posts_period.start / posts_period.end
posts = get_data(POSTS)
if posts is not None:
seconds = pl.col('created_at').dt.epoch('s')
count = posts.filter(seconds.is_between(START, END, closed='left')).height
hourly = posts.group_by_dynamic('created_at', every='1h').agg(pl.len())
by_et_hour = posts.group_by(
pl.col('created_at').dt.convert_time_zone('America/New_York').dt.hour()
).agg(pl.len())
Copy the period from an exact Polymarket event lookup (posts_period.start /
posts_period.end); it is the market's own counting window, not the listing
startDate. Runtime capability: x_posts_reference_v5.
New posts, confirmations and withdrawals update the frame. Normal tick
strategies read the latest frame on their next evaluation. Annotated tick
strategies can declare
@on_market(POSTS, reads=[POSTS, MARKET], trades=[MARKET], primary=MARKET) to
evaluate on each X change without waiting for a price tick. X keys are
reference reads only: invalid as universe entries, order targets or primary
markets. See collection and deployment contract.
Keyed weather data (Live / Paper)
Weather uses the same get_data() function with a read-only region key;
it is not a trading ticker and must not be added to universe.
WEATHER = 'OPENMETEO:KR/Seoul'
weather = get_data(WEATHER)
temperature = weather.current.temperature
if temperature is not None:
degrees_c = temperature.celsius
degrees_f = temperature.fahrenheit
value, unit = temperature.value, temperature.unit
today_high = weather.daily[0].temperature_max
if today_high is not None:
today_high_c = today_high.celsius
- Preferred keys are
OPENMETEO:COUNTRY/City, using a two-letter country code and the full English city name:OPENMETEO:KR/Seoul,OPENMETEO:JP/Tokyo,OPENMETEO:GB/London. For same-name cities, include the full first-level administrative region:OPENMETEO:US/Illinois/Springfield. - Names are case-insensitive. Spaces and underscores are equivalent:
OPENMETEO:US/New York/New YorkandOPENMETEO:US/New_York/New_Yorkuse the same key (New York city in New York state). Region keys canonicalize to uppercase (OPENMETEO:KR/SEOUL). Unknown or ambiguous names return an error during subscription preparation; no arbitrary first search result or guessed coordinate is used. - Publisher resolves a region once per active shared subscription using Open-Meteo geocoding, then reuses its coordinates for forecast collection. Concurrent subscribers share the lookup. Region names remain the identity in the manifest, SHM and Runner; no geocoding occurs in the compiler or during strategy evaluation.
- Explicit coordinates remain available for precise locations, for example
OPENMETEO:37.5665,126.9780(up to six fractional digits). A city key denotes the provider's city location, not a user-selected station. Coordinate and region keys are distinct subscriptions; no implicit alias guessing is done. - Literal keys and constant aliases are compiled into
reference_keys; an unused weather literal does not create a subscription. current.temperatureand each hourlytemperaturecontainvalueandunit(celsius/fahrenheit)..celsiusand.fahrenheitconvert locally. Missing provider values remainNone; check before accessing a temperature.hourlycontains the seven-day hourly forecast;dailycontains seven local dates withtemperature_max/temperature_min.current.timeand hourlytimeare epoch milliseconds;daily.dateisYYYY-MM-DDintimezone.received_atis the forecast retrieval time in epoch milliseconds.- Publisher collects one shared snapshot per canonical key every 15 minutes while subscribed. Subscriber commits it to SHM; Runner reads that SHM key, with no network calls from DSL/evaluation. Repeated reads within one evaluation use the same snapshot. Last-owner release expires the shared subscription.
- Both tick and candle strategies may read weather. In
@on_marketlanes, declare the key inreads=[WEATHER]; market events still select the lane. Weather keys cannot be order targets or candle timeframes. - These are forward forecast snapshots, not historical observations. They are not a historical backtest/replay data source.
Available regions and verified keys
There is no fixed city whitelist. A key is supported when Open-Meteo returns one populated-place record matching the full English city name, country, and optional first-level administrative region. The current key grammar accepts ASCII English names, not arbitrary localized names or accented characters. The following 36 example keys were checked against the public geocoding API on 2026-09-16, using the same matching rules as Publisher. This verifies location resolution, not continuous forecast availability or a complete world city catalog. Unlisted cities can also be used if they resolve unambiguously.
| Region | DSL key |
|---|---|
| Seoul / 서울 | OPENMETEO:KR/Seoul |
| Busan / 부산 | OPENMETEO:KR/Busan/Busan |
| Incheon / 인천 | OPENMETEO:KR/Incheon/Incheon |
| Tokyo / 도쿄 | OPENMETEO:JP/Tokyo |
| Osaka / 오사카 | OPENMETEO:JP/Osaka/Osaka |
| Taipei / 타이베이 | OPENMETEO:TW/Taipei |
| Hong Kong / 홍콩 | OPENMETEO:HK/Hong Kong |
| Singapore / 싱가포르 | OPENMETEO:SG/Singapore |
| Bangkok / 방콕 | OPENMETEO:TH/Bangkok |
| Beijing / 베이징 | OPENMETEO:CN/Beijing Municipality/Beijing |
| Shanghai / 상하이 | OPENMETEO:CN/Shanghai Municipality/Shanghai |
| Mumbai / 뭄바이 | OPENMETEO:IN/Mumbai |
| London / 런던 | OPENMETEO:GB/London |
| Paris / 파리 | OPENMETEO:FR/Paris |
| Berlin / 베를린 | OPENMETEO:DE/State of Berlin/Berlin |
| Madrid / 마드리드 | OPENMETEO:ES/Madrid |
| Rome / 로마 | OPENMETEO:IT/Rome |
| Amsterdam / 암스테르담 | OPENMETEO:NL/Amsterdam |
| Istanbul / 이스탄불 | OPENMETEO:TR/Istanbul |
| New York / 뉴욕 | OPENMETEO:US/New York/New York |
| Chicago / 시카고 | OPENMETEO:US/Illinois/Chicago |
| Los Angeles / 로스앤젤레스 | OPENMETEO:US/California/Los Angeles |
| Miami / 마이애미 | OPENMETEO:US/Florida/Miami |
| Dallas / 댈러스 | OPENMETEO:US/Texas/Dallas |
| Austin / 오스틴 | OPENMETEO:US/Texas/Austin |
| Atlanta / 애틀랜타 | OPENMETEO:US/Georgia/Atlanta |
| Seattle / 시애틀 | OPENMETEO:US/Washington/Seattle |
| Boston / 보스턴 | OPENMETEO:US/Massachusetts/Boston |
| Denver / 덴버 | OPENMETEO:US/Colorado/Denver |
| San Francisco / 샌프란시스코 | OPENMETEO:US/California/San Francisco |
| Toronto / 토론토 | OPENMETEO:CA/Ontario/Toronto |
| Vancouver / 밴쿠버 | OPENMETEO:CA/British Columbia/Vancouver |
| Sydney / 시드니 | OPENMETEO:AU/New South Wales/Sydney |
| Melbourne / 멜버른 | OPENMETEO:AU/Victoria/Melbourne |
| Auckland / 오클랜드 | OPENMETEO:NZ/Auckland |
| Wellington / 웰링턴 | OPENMETEO:NZ/Wellington |
Use these qualified spellings rather than assuming a famous city's short key
is unique. For example, KR/Busan matched two places and JP/Osaka matched six.
VN/Hanoi/Hanoi still matched two places even with admin1; it is not in
the verified list. If country/admin1/city remains ambiguous, supply explicitly
chosen coordinates rather than expecting the system to guess a location.
See the provider's geocoding contract.
Data available in the current DSL integration
Let w = get_data('OPENMETEO:KR/Seoul'). These are the implemented fields,
not the entire Open-Meteo product catalog:
| Meaning | DSL field | Scope / semantics |
|---|---|---|
| Current temperature | w.current.temperature | Temperature at 2 m above ground, model-derived current conditions |
| Current valid time | w.current.time | Epoch milliseconds for those current conditions |
| Today's forecast maximum | w.daily[0].temperature_max | Maximum for the first local calendar date in this snapshot |
| Today's forecast minimum | w.daily[0].temperature_min | Minimum for the same date |
| Tomorrow's forecast maximum / minimum | w.daily[1].temperature_max, w.daily[1].temperature_min | Second local calendar date |
| Daily maximum / minimum forecasts | w.daily[i].temperature_max, w.daily[i].temperature_min | Seven local dates starting with the retrieval day's local today; normally indices 0–6 |
| Target date | w.daily[i].date | YYYY-MM-DD in the location's time zone; use this when selecting a specific date |
| Hourly temperature forecast | w.hourly[i].temperature | Hourly series for the seven-day window, starting at local midnight, not the next hour |
| Hourly forecast valid time | w.hourly[i].time | Epoch milliseconds; select by time rather than assuming hourly[0] means now |
| Location time zone | w.timezone | IANA name, e.g. Asia/Seoul |
| Snapshot retrieval time | w.received_at | Epoch milliseconds when Publisher retrieved this forecast; not the model's issuance time |
Every non-null temperature exposes .value, .unit, .celsius, and
.fahrenheit. Publisher currently requests Celsius; Fahrenheit access is a
local conversion. The series are read-only and a missing temperature is None,
not zero. The request window is seven days; use the returned dates/timestamps
and lengths rather than assuming fixed hour counts across daylight-saving time.
The distinction between a forecast for a day and a forecast saved at a past time matters:
- "What is today's expected maximum now?" — available through the current
snapshot's
dailyrow for today. This is not the highest observed reading so far today or a final observed daily maximum. - "What did yesterday's 09:00 forecast predict for today?" — not exposed. This feed retains the latest snapshot, not an archive of past forecast runs.
- "What is the current measured station temperature?" — not guaranteed by
current.temperature: Open-Meteo current conditions are based on model data, not a direct weather-station observation stream.
Publisher refreshes every 15 minutes while subscribed. That is the collection
cadence, not a promise of a new model run every 15 minutes. Use received_at,
current.time and the target date to interpret freshness, including around
local midnight. These definitions follow the
Open-Meteo forecast documentation.
Not currently connected to DSL: observed daily records, past forecast vintages, model run/issuance metadata, ensemble distributions or temperature threshold probabilities, humidity, rain/precipitation, wind, cloud cover, pressure, and apparent temperature. Open-Meteo offers additional weather variables and forecast products, but this implementation only requests current temperature, hourly temperature, and daily maximum/minimum temperature. The other products are not silently fetched or implied to be available.
get_data(...) Return Object
get_data(...) returns an object with both attribute and key access:
x.closeorx["close"]x.open,x.high,x.low,x.volumex.priceorx["price"]: a scalar current price for this ticker at the evaluation boundary;Noneif the current snapshot has no price. This does not change with the requested candle timeframe.
market = get_data('BINANCE:BTC/USDT')
latest = market.price
four_hour_close = get_data('BINANCE:BTC/USDT', '4h').close[-1]
LIVE uses its existing market snapshot. Candle backtests use the current simulated
price, not a future row or the last close of the requested higher timeframe.
Scripts reading .price use the existing ordered backtest evaluator so scalar
if/and expressions have the same meaning as LIVE. OHLCV Series remain unchanged.
The existing bare price name is retained for compatibility; use get_data(ticker).price
when reading multiple markets.
When timeframe differs from the strategy timeframe, the returned OHLCV series
remain on that timeframe's native candle grid. Calculate every remote indicator
and condition on those native series first, then pass only the calculated result
to align_timeframe(...) before combining it with current-timeframe series.
Raw series from different timeframes must not be combined directly.
daily = get_data(context['ticker'], timeframe='1d')
daily_uptrend_native = ema(daily.close, 8) > ema(daily.close, 21)
daily_uptrend = align_timeframe(
daily,
daily_uptrend_native,
timeframe='1d',
base_time=time,
)
emit(daily_uptrend & (rsi(close, 14) < 30), entry(context['ticker'], 'LONG', Weight(1.0)))
The default merge uses confirmed-bar semantics (lookahead_off, gaps_off):
a remote result first becomes visible at its time_close and is then carried
forward until the next confirmed remote result. In tick lanes, scalar reads such
as get_data(ticker, timeframe='1m').close[-1] continue to read the latest
available candle history and do not require a vector merge.
Live-only Extended Fields
When the selected market supports them, the same object also exposes:
| Field | Type | Availability | Notes |
|---|---|---|---|
.funding_rate | scalar or series | Perpetual | Live: scalar ticker info, Backtest: aligned series when funding data exists |
.mark_price | scalar or series | Perpetual | Live: scalar ticker info, Backtest: aligned series when available |
.index_price | scalar or series | Perpetual | Live: scalar ticker info, Backtest: aligned series when available |
.book | orderbook object or None | Live only | Backtest returns None |
Orderbook Object (.book) Fields
If .book is not None, these fields are available:
best_bidbest_askbest_bid_qtybest_ask_qtymid_pricespreadtimestampbids(top levels)asks(top levels)
Signal Emission
| Function | Signature | Description |
|---|---|---|
emit | emit(condition, action) | Submit one order action when the condition is true |
emit_chain | emit_chain(condition, actions) | Submit a sequential group; later actions can use Filled(label) |
emit_oco | emit_oco(condition, actions) | Submit a one-cancels-other group |
A complete executable script must call at least one emit* function or a portfolio rebalance helper such as rebalance_to(...) or rebalance_market_neutral(...).
Bare entry(...), exit_position(...), and adjust(...) calls create order actions; they do not submit orders on their own.
To submit several orders from one strategy evaluation, call emit(...) once for each action with the same condition. The resulting orders are independent: exchanges cannot provide a cross-venue all-or-nothing transaction, so multi-leg strategies must handle partial fills, rejects, and leg recovery explicitly.
Order Construction
| Function | Signature | Description |
|---|---|---|
entry | entry(asset, direction, sizing, ..., leverage=None, margin_type=None, when=None, label=None, memo=None, accumulate=False, take_profits=None, trigger_price=None) | Open or add to a position, with optional per-order leverage and explicit margin type for perpetual markets |
exit_position | exit_position(asset, sizing=None, execution='MARKET', limit_price=None, ..., when=None, label=None, memo=None) | Close a position; sizing defaults to FullExit(). Use PartialExit(ratio) for partial closes. |
adjust | adjust(asset, target_weight, direction='LONG', leverage=None, margin_type=None, when=None, label=None, memo=None) | Rebalance to target weight with optional per-order leverage and explicit margin type for perpetual markets |
Sizing Types
| Type | Signature |
|---|---|
| Weight | Weight(value: float) |
| Quantity | Quantity(value: float) |
| Notional | Notional(value: float) |
Notional(n) is expressed in the instrument's quote currency: USDC for
POLYMARKET:…/USDC, USDT for BTC/USDT, and BTC for ETH/BTC. It is not an
automatic USD conversion. The DSL emits the budget without resolving amount.
The Runner broker binds quantity at submission preparation: LIMIT/STOP_LIMIT use
the declared limit price, STOP/TAKE_PROFIT market use the trigger price, and
MARKET uses the observed reference price. Inverse contracts use their authoritative
contract face value. Venue quantity steps are then applied. These rules are shared
by Live, Paper, and Backtest; they do not guarantee an exact eventual fill value.
The original budget remains in Action and Signal. Polymarket market-firing BUY orders (including STOP market BUY)
use that original value as the existing quote_amount budget. Quantity(q)
specifies order units and does not invent a quote budget.
Leverage semantics:
Weight(w)+leverage=x→ scales target exposure by leverageNotional(n)+leverage=x→ keeps exposure atn, while leverage affects margin / collateral usageQuantity(q)+leverage=x→ keeps position quantity atq, while leverage affects margin / collateral usage
margin_type is optional and accepts CROSS or ISOLATED. Venues that require
leverage and margin mode to change atomically require both values to be supplied;
the DSL does not guess a venue-specific default. If neither the intent nor the
policy config specifies leverage, the order leaves the account's leverage and
margin mode unchanged.
Stop/Take-Profit Types
Used in entry(...) as stop_loss= and take_profit= parameters. Live routing
can be exchange-native or HeyTraders-managed. Before live use, inspect the
selected account and symbol through the caller's current Order capability
surface; do not infer support from the DSL type alone.
| Type | Signature | Description |
|---|---|---|
PercentFromEntry | PercentFromEntry(pct) | Percent from entry price. -0.05 = 5% SL, 0.10 = 10% TP |
ATRMultiple | ATRMultiple(multiple, period=14) | Dynamic level at multiple × ATR from entry |
AbsolutePrice | AbsolutePrice(price) | Fixed price level |
TickOffsetFromEntry | TickOffsetFromEntry(ticks) | Tick offset from entry price |
TrailingStop | TrailingStop(trail_percent, activation_percent=0.0, initial_stop_percent=None) | Percentage-point trailing stop (3 = 3%): tracks highest/lowest price and closes on pullback |
TrailingATR | TrailingATR(multiple, activation_percent=0.0, initial_stop_percent=None, period=14) | ATR-based trailing; activation_percent and initial_stop_percent use percentage points |
TakeProfitLevelSpec | TakeProfitLevelSpec(price, amount=None, limit_price=None, stop_loss_price=None, pct=None) | Scaled TP level: trigger price plus exactly one of absolute amount or filled-position percentage pct; optional limit and per-level stop price |
Execution Modes
| Mode | Value | Description |
|---|---|---|
MARKET | Market order | Default |
LIMIT | Limit order | Requires limit_price |
STOP | Stop-triggered market order | Requires trigger_price |
STOP_LIMIT | Stop-triggered limit order | Requires trigger_price and limit_price |
PASSIVE | Limit + PostOnly | Maker only |
IOC | Limit + IOC | Immediate or cancel |
FOK | Limit + FOK | Fill or kill |
Signal DSL order emission does not currently select TWAP or VWAP. Submit those executions through the Advanced Order ticket or order API. Iceberg is not simulated; use it only through the order API when the selected account and symbol report native iceberg capability.
Scaled Take-Profit (Multi-TP)
Split take-profit across multiple price levels. Each level specifies a trigger price
and exactly one of an absolute quantity (amount) or a percentage of the filled
position (pct). A level may also specify an optional limit price. Scaled TP is
mutually exclusive with single take_profit.
Backtests execute these levels against candle OHLC data. A filled partial TP reduces the remaining position and its stop-loss budget. Execution instructions that the candle model cannot represent are rejected instead of being silently approximated.
# 3-level scaled TP: 50% at $100K, 30% at $105K, 20% at $110K
emit(
True,
entry(
'BINANCEFUTURES:BTC/USDT:USDT',
'LONG',
Quantity(1.0),
stop_loss=PercentFromEntry(-0.05),
take_profits=[
TakeProfitLevelSpec(100000, pct=50),
TakeProfitLevelSpec(105000, pct=30),
TakeProfitLevelSpec(110000, pct=20),
],
),
)
# With limit price on final level
emit(
True,
entry(
'BINANCEFUTURES:BTC/USDT:USDT',
'LONG',
Quantity(1.0),
take_profits=[
TakeProfitLevelSpec(100000, pct=50),
TakeProfitLevelSpec(110000, limit_price=109500, pct=50),
],
),
)
See Orders for the public order documentation.
Validation Rules
The following combinations are rejected while an order action is constructed:
EXIT constraints:
- EXIT +
stop_loss-- Error: closing orders cannot have stop loss - EXIT +
take_profit-- Error: closing orders cannot have take profit - EXIT +
take_profits-- Error: closing orders cannot have scaled take profit
Execution constraints:
LIMIT/IOC/FOKwithoutlimit_price-- ErrorSTOPwithouttrigger_price-- ErrorSTOP_LIMITwithouttrigger_priceorlimit_price-- Error
Mutual exclusion:
take_profit+take_profits-- Error: use one or the other
Cross-dimension constraints:
PASSIVE+stop_loss-- Error: PostOnly contradicts trigger ordersPASSIVE+take_profit-- Error: PostOnly contradicts trigger ordersSTOP/STOP_LIMIT+ attachedstop_loss/take_profit-- currently rejected; use a separate exit/protection path until combined semantics are expanded
Order-submission validation:
- Market + PostOnly -- Error: contradictory (taker vs maker)
- Market + GTC -- Error: market orders execute immediately
- StopLoss/TakeProfit + PostOnly -- Error: contradictory
See Orders for public order constraints.
Execution Conditions
| Condition | Description |
|---|---|
NoPosition(asset=None) | No position exists for the selected asset |
HasPosition(asset=None, side=None) | Position exists for the selected asset (optional LONG or SHORT side filter) |
MaxExposure(max_ratio) | Exposure <= ratio |
MinBalance(min_amount) | Free balance of the order ticker's quote/settlement asset in its account >= min_amount |
Filled(label) | True after labeled order is filled |
HedgeOnFill(label) | In an emit_chain, Broker-manage a proportional hedge for each authoritative fill delta of the referenced source order |
AllOf(...) | Logical AND of conditions |
AnyOf(...) | Logical OR of conditions |
WARNING: NoPosition(), HasPosition() etc. are NOT boolean values. They MUST be passed via the
when=parameter of entry()/exit_position()/adjust(). Do NOT use them in boolean expressions with & or |. WRONG:emit((rsi <= 30) & (NoPosition()), entry(ticker, 'LONG', Weight(1.0)))CORRECT:emit(rsi <= 30, entry(ticker, 'LONG', Weight(1.0), when=NoPosition()))
For NoPosition and HasPosition, an omitted asset uses the action's
selected asset. Pass the side as a keyword, for example
HasPosition(asset=ticker, side='LONG'); the first positional argument is an
asset, not a side.
HedgeOnFill(label) is a typed deployment contract, not a local polling
condition. The referenced action is the source order in the same emit_chain,
and the conditioned action is the hedge. The source may use any supported
order execution (market, limit, trigger, sliced, iceberg, or other validated
Broker order shape) and its own time-in-force; HedgeOnFill does not force
Maker, Limit, or PostOnly. The Live Runner submits the pair once; PAPER and
LIVE Brokers use the same schema, graph lifecycle, idempotency, and fill-delta
sizing.
PASSIVE remains the strict post-only shorthand when the source strategy
chooses it; LIMIT remains a normal limit order and may be marketable. GTC,
GTT, and PostOnly are order settings, not HedgeOnFill behavior. Venue
capability validation and any source-order constraints still apply exactly as
they do to the corresponding ordinary order.
The canonical deployed signal field is hedge_on_fill. HedgeOnFill is
distinct from the Broker's internal activate_on_fill graph edge: the latter
activates one predeclared downstream node, while HedgeOnFill creates a new
hedge for each newly observed source-order fill delta.
Wallet Tracking Operators (Live Context)
These functions are available in the DSL operator registry:
| Function | Signature | Description |
|---|---|---|
wallet_filled | wallet_filled(address: str, coin: str = None) | Whether tracked wallet has a fresh fill |
wallet_side | wallet_side(address: str, coin: str = None) | Latest fill side (BUY/SELL/NONE) |
wallet_price | wallet_price(address: str, coin: str = None) | Latest fill price |
wallet_size | wallet_size(address: str, coin: str = None) | Latest fill size |
Wallet tracking is live-only (Hyperliquid, tick mode). In backtest, safe defaults are returned.
Exchange Constraint Helpers
These helpers expose exchange tick/step constraints directly inside the DSL.
| Function | Signature | Description |
|---|---|---|
tick_size | tick_size(ticker, price=None) | Minimum price increment when exchange metadata is available. |
step_size | step_size(ticker) | Minimum quantity increment. |
min_qty | min_qty(ticker) | Minimum order quantity when metadata exists, else None. |
min_notional | min_notional(ticker) | Minimum notional when metadata exists, else None. |
price_precision | price_precision(ticker, price=None) | Decimal places implied by price tick when available. |
qty_precision | qty_precision(ticker) | Decimal places implied by quantity step when available. |
round_price | round_price(ticker, price, side=None, mode='exchange') | Snap price to valid tick. exchange mode uses BUY/LONG=floor, SELL/SHORT=ceil. |
round_qty | round_qty(ticker, qty, mode='down') | Snap quantity to valid step size. |
is_valid_price | is_valid_price(ticker, price) | Whether price is already tick-aligned. |
is_valid_qty | is_valid_qty(ticker, qty) | Whether quantity is already step-aligned. |
is_valid_notional | is_valid_notional(ticker, qty, price) | Whether notional passes min-notional metadata check when available. |
ticks_to_price | ticks_to_price(ticker, ticks, price=None) | Convert tick distance to absolute price distance. |
price_to_ticks | price_to_ticks(ticker, price_diff, price=None) | Convert absolute price distance to tick count. |
qty_for_notional | qty_for_notional(ticker, notional, price) | Convert notional to quantity, rounded to valid size step. |
Notes:
- These helpers are recommended for grid and passive quoting strategies.
- Some live order paths can also adjust price or size at execution time. For deterministic strategy logic, use the helpers explicitly in the DSL.
book = get_data(context['ticker']).book
mid = book.mid_price
bid_px = round_price(context['ticker'], mid - ticks_to_price(context['ticker'], 5), side='BUY')
ask_px = round_price(context['ticker'], mid + ticks_to_price(context['ticker'], 5), side='SELL')
qty = round_qty(context['ticker'], qty_for_notional(context['ticker'], 100, mid))
# Passive quoting example
emit(True, entry(
context['ticker'],
'LONG',
Quantity(qty),
execution='PASSIVE',
limit_price=bid_px,
label='mm_bid'
))
Cross-Sectional Helpers
These helpers are intended for cross mode portfolio strategies.
| Function | Signature | Description |
|---|---|---|
rank_desc | rank_desc(scores) | Rank tickers by descending score. |
rank_asc | rank_asc(scores) | Rank tickers by ascending score. |
top_n / bottom_n | top_n(scores, n) / bottom_n(scores, n) | Select top/bottom N tickers. |
top_pct / bottom_pct | top_pct(scores, pct) / bottom_pct(scores, pct) | Select top/bottom percentile of tickers. |
drop_nan | drop_nan(scores) | Remove non-finite scores. |
cs_demean | cs_demean(scores) | Subtract cross-sectional mean. |
cs_rank | cs_rank(scores) | Convert scores to percentile ranks. |
cs_zscore | cs_zscore(scores) | Cross-sectional z-score normalize. |
cs_clip | cs_clip(scores, min_value, max_value) | Clip scores to a bounded range. |
equal_weight | equal_weight(tickers, gross=1.0, side='LONG') | Build equal-weight portfolio for a side. |
long_short_equal_weight | long_short_equal_weight(longs, shorts, gross=1.0) | Build market-neutral long/short equal weights. |
weights_from_scores | weights_from_scores(scores, gross=1.0, demean=True) | Convert scores to gross-normalized weights. |
normalize_gross | normalize_gross(weights, gross=1.0) | Scale weights to target gross exposure. |
cap_weights | cap_weights(weights, max_abs_weight) | Cap per-asset absolute weight. |
drop_small_weights | drop_small_weights(weights, min_abs_weight) | Remove tiny target weights. |
merge_weights | merge_weights(*weight_maps) | Merge multiple weight maps by summing. |
current_weight | current_weight(ticker) | Estimated current weight for a ticker. |
current_weights | current_weights(tickers=None) | Estimated current weights map. |
portfolio_value | portfolio_value(ticker=None) | Equity in the ticker's account and quote/settlement asset; omit ticker only for a single funding pool. None when a held position cannot be valued; guard it before arithmetic. |
gross_exposure | gross_exposure() | Estimated gross exposure. |
net_exposure | net_exposure() | Estimated net exposure. |
free_balance | free_balance(currency=None) | Free balance of currency (default: the quote/settlement asset of the evaluated ticker) in the one account that trades the evaluation scope; the total when the venue reports no free amount. Raises when the scope spans several accounts. |
rebalance_to | rebalance_to(weights, leverage=None, min_change=0.0, drop_missing_to_zero=True) | Rebalance to target weights. |
rebalance_market_neutral | rebalance_market_neutral(longs, shorts, gross=1.0, max_abs_weight=None, min_change=0.0, leverage=None) | Convenience helper for market-neutral rebalance. |
In Live, rebalance_to resolves target quantities using native funding-pool equity and
subtracts the strategy's current filled quantities. Linear perpetual positions
are revalued at the evaluation price rather than a cached unrealized PnL.
The resulting orders retain their quantities and ordinary Broker validation.
A funding pool is an execution account plus the instrument's quote asset
(spot) or settlement asset (derivatives). USDT, USDC, BTC and all other assets
remain separate balances. For example, ETH/BTC targets use BTC equity; USDT in
another pool is not converted into spendable BTC. A weight applies to the
target instrument's own pool. portfolio_value('BINANCE:ETH/BTC') returns BTC
units. Same-currency balances on different accounts are not pooled for sizing.
These helpers do not issue conversions, transfers or borrowings. Inverse
contract target sizing requires contract metadata that this helper currently
does not receive; it reports an error instead of applying linear sizing.
For a rebalance containing reductions and increases on the same account, the Runner compares the increase phase's estimated collateral and fees with free balances, separately by asset. If all increases fit and no outstanding orders make the balance uncertain, reductions and increases are handed off concurrently. Otherwise increases retain the reduction-fill dependency. Reduction-only and increase-only batches need no phase barrier; accounts execute independently. Scheduling does not change signal identities or retry a rejected order.
This is a scheduling estimate, not a guarantee of venue acceptance. Inverse contract collateral, unsupported collateral conversions, missing prices, and unresolved orders do not use the concurrent fast path. Market price changes, actual fees, and activity elsewhere on the account can still affect acceptance.
scores = {}
for ticker in context['tickers']:
data = get_data(ticker)
scores[ticker] = (data.close[-1] - data.close[0]) / data.close[0]
scores = drop_nan(scores)
longs = top_n(scores, 5)
shorts = bottom_n(scores, 5)
target_basket_notional = 1000.0
equity = portfolio_value()
target_gross = min(target_basket_notional / equity, 1.0) if equity else 0.0
weights = long_short_equal_weight(longs, shorts, gross=target_gross)
weights = cap_weights(weights, 0.15)
rebalance_to(weights, min_change=0.02, leverage=2)
7) Script Syntax Constraints
- Use vectorized series operations for signal logic.
- For series booleans, prefer bitwise operators (
&,|,~). Pythonand/or/notare automatically converted to&/|/~by the preprocessor, but explicit bitwise is recommended for clarity. - No pandas APIs (
.rolling(),.iloc, dataframe indexing patterns). - Use DSL operator functions from
/v1/docs/operators.
8) Order Management Functions
These functions manage open orders directly within the script.
| Function | Signature | Return | Description |
|---|---|---|---|
cancel_order | cancel_order(label: str) | bool | Cancel pending order by label |
has_open_order | has_open_order(label: str) | bool | Check pending order existence |
get_open_orders | get_open_orders(ticker: str | None = None, side: str | None = None, prefix: str | None = None) | list[dict] | List pending/open orders |
get_open_order | get_open_order(label: str) | dict | None | Get one pending/open order by label |
replace_order | replace_order(label: str, price: float | None = None, size: float | None = None, weight: float | None = None) | bool | Cancel + recreate pending order |
amend_order | amend_order(label: str, size: float | None = None, weight: float | None = None) | bool | In-place size amendment (no price change) |
Constraints:
- Max 50 order-management calls per step
- Label: max 64 chars,
[a-zA-Z0-9_-] sizeandweightare mutually exclusive
If runtime open-order state is available, get_open_orders() / get_open_order() may include:
account_idorder_idlabelassettickersidestatuspricesizeremaining_sizefilled_sizefill_ratioorder_typetime_in_forcememocreated_at_msupdated_at_msage_ms
# Grid-style requote
order = get_open_order('grid_l1')
if order is not None:
new_price = round_price(context['ticker'], order['price'] + tick_size(context['ticker']), side='BUY')
replace_order('grid_l1', price=new_price)
9) Strategy Configuration Fields
| Field | Type | Required | Description |
|---|---|---|---|
script | string | Yes | Signal DSL script |
exchange | string | Yes | Strategy metadata / primary venue hint (not a ticker fallback) |
universe | string[] | Yes | Full tickers to load and evaluate (EXCHANGE:BASE/QUOTE[:SETTLE]) |
timeframe | string | Yes | Candle: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M; live-only: tick |
mode | string | No | isolated (default) or cross |
leverage | number | No | Optional leverage value for perpetual strategies |
strategy_name | string | No | Human-readable strategy name |
10) Backtest vs Live Notes
| Feature | Backtest | Live |
|---|---|---|
emit_oco | Supported | Supported |
emit_chain + Filled(label) | Supported | Supported |
emit_chain + HedgeOnFill(label) | Not modeled | Supported in PAPER and LIVE deployment modes |
cancel_order / replace_order | Modeled by the strategy runtime | Capability-driven for the selected account and symbol |
get_data(...).book | Returns None | Returns live orderbook |
wallet_* operators | Returns safe defaults | Returns live wallet data (Hyperliquid, tick mode) |
| Order time in force | Modeled by the backtest engine | Capability-driven for the selected account and symbol |
take_profits (scaled TP) | Supported by the candle execution model (amount or pct) | Supported (proportional partial fill) |