HeyTraders Documentation

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:

  1. Choose the strategy type and read its current input contract.
  2. Write the script with the ticker, data, operator, and sizing rules below.
  3. Submit the requested backtest or live-strategy action through that surface.
  4. 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]

FormatMarket TypeExample
EXCHANGE:BASE/QUOTESpotBINANCE:BTC/USDT
EXCHANGE:BASE/QUOTE:SETTLEPerpetualBINANCEFUTURES:BTC/USDT:USDT
EXCHANGE.namespace:BASE/QUOTE:SETTLENamespaced perpetualHYPERLIQUID.xyz:AAPL/USDC:USDC
  • :SETTLE suffix 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 as CHAINLINK:BTC/USD are declared in an annotated lane's reads, not added to the trading universe.
  • Binance Futures public examples use BINANCEFUTURES:BASE/QUOTE:SETTLE. Concrete data/chart prefixes BINANCEFUTURESUSD:BASE/QUOTE:SETTLE and BINANCEFUTURESCOIN:BASE/QUOTE:SETTLE are 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:USDC
    • HYPERLIQUID.xyz:AAPL/USDC:USDC
    • HYPERLIQUID.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 IDTypeBacktestPaperTrade
binanceSpotYesYesYes
binancefuturesPerpetualYesYesYes
hyperliquidPerpetual (supports .namespace)YesYesYes
lighterPerpetualYesYesYes
extendedPerpetualYesYesYes
polymarketperpPerpetualNoYesYes
polymarketPrediction marketYesYesYes

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 IDTypeExample
fxForexFX:EUR/USD
indexIndicesINDEX:^IXIC
yahooMulti-asset data sourceYAHOO:AAPL
kiskrKR stock dataKISKR:005930
kisusUS stock dataKISUS:AAPL

3) Execution Modes

ModeBehaviorAccess Pattern
isolated (default)Run same logic per tickerUse auto-injected series (close, high, ...)
crossEvaluate all tickers togetherUse 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 to entry(...), 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 dynamic get_data() markets that cannot be inferred statically.
  • trades=[...] declares dynamic order targets such as rebalance_to() universes.
  • trades does not grant get_data() access; a market used by both must also be declared in reads.
  • primary=... anchors the data fields available for that cadence and context['ticker']; it defaults to the first wake market. Candle lanes can use bare close, while tick lanes use scalar price and do not receive bare OHLCV.
  • Traded wake/read/trade markets must be present in universe. Read-only reference keys such as Chainlink are declared in reads but 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).price reads 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_index is not available. Count bars on an explicit candle instead, e.g. len(get_data(ticker, '1m').time_close).

5) Built-in Variables

CategoryVariables
OHLCV seriesopen, high, low, close, volume
Time seriestime, time_close, hour, minute, weekday
Tick modeprice
Contextcontext["ticker"], context["tickers"]
Position stateposition, entry_count, avg_price, position_opened_at, unrealized_pnl, current_price, funding_rate
Accountbalance
Contract pricesmark_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.

FunctionDescription
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.

QueryOne row isTypical 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:

QueryFields
fillsid, order_id, label, account_id, exchange, ticker, side, quantity, price, fee, fee_asset, timestamp, status (pending/confirmed/failed), pending, position_effect, realized_pnl
entriesorder_id (also id), label, account_id, ticker, side, quantity, timestamp, pending
tradesround_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)
settlementsid, 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_pnl is the exchange-side value net of a fee paid in the quote currency. A trade's realized_pnl sums its fills and settlement. When any part is not established, reading it raises an error instead of returning 0.
  • Polymarket fills are pending from MATCHED until CONFIRMED, and a failed match disappears. In Live and Paper, positions, and therefore HasPosition/NoPosition and sells, follow confirmed shares only; a pending buy already counts in history.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

FunctionSignatureNotes
get_dataget_data(ticker: str, timeframe: str = current)Required in cross mode for external series access
align_timeframealign_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_positionget_position(ticker: str) -> floatThe 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_priceget_entry_price(ticker: str) -> float | NoneAverage 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.

FunctionContract
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, and adjust require outcome for a market slug. limit_price and 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 as NoPosition(), 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_recurrence reports 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 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:

columntypemeaning
created_atDatetime('ms', 'UTC')post creation time
statusstr'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 York and OPENMETEO:US/New_York/New_York use 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.temperature and each hourly temperature contain value and unit (celsius / fahrenheit). .celsius and .fahrenheit convert locally. Missing provider values remain None; check before accessing a temperature.
  • hourly contains the seven-day hourly forecast; daily contains seven local dates with temperature_max / temperature_min. current.time and hourly time are epoch milliseconds; daily.date is YYYY-MM-DD in timezone. received_at is 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_market lanes, declare the key in reads=[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.

RegionDSL 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:

MeaningDSL fieldScope / semantics
Current temperaturew.current.temperatureTemperature at 2 m above ground, model-derived current conditions
Current valid timew.current.timeEpoch milliseconds for those current conditions
Today's forecast maximumw.daily[0].temperature_maxMaximum for the first local calendar date in this snapshot
Today's forecast minimumw.daily[0].temperature_minMinimum for the same date
Tomorrow's forecast maximum / minimumw.daily[1].temperature_max, w.daily[1].temperature_minSecond local calendar date
Daily maximum / minimum forecastsw.daily[i].temperature_max, w.daily[i].temperature_minSeven local dates starting with the retrieval day's local today; normally indices 0–6
Target datew.daily[i].dateYYYY-MM-DD in the location's time zone; use this when selecting a specific date
Hourly temperature forecastw.hourly[i].temperatureHourly series for the seven-day window, starting at local midnight, not the next hour
Hourly forecast valid timew.hourly[i].timeEpoch milliseconds; select by time rather than assuming hourly[0] means now
Location time zonew.timezoneIANA name, e.g. Asia/Seoul
Snapshot retrieval timew.received_atEpoch 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 daily row 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.close or x["close"]
  • x.open, x.high, x.low, x.volume
  • x.price or x["price"]: a scalar current price for this ticker at the evaluation boundary; None if 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:

FieldTypeAvailabilityNotes
.funding_ratescalar or seriesPerpetualLive: scalar ticker info, Backtest: aligned series when funding data exists
.mark_pricescalar or seriesPerpetualLive: scalar ticker info, Backtest: aligned series when available
.index_pricescalar or seriesPerpetualLive: scalar ticker info, Backtest: aligned series when available
.bookorderbook object or NoneLive onlyBacktest returns None

Orderbook Object (.book) Fields

If .book is not None, these fields are available:

  • best_bid
  • best_ask
  • best_bid_qty
  • best_ask_qty
  • mid_price
  • spread
  • timestamp
  • bids (top levels)
  • asks (top levels)

Signal Emission

FunctionSignatureDescription
emitemit(condition, action)Submit one order action when the condition is true
emit_chainemit_chain(condition, actions)Submit a sequential group; later actions can use Filled(label)
emit_ocoemit_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

FunctionSignatureDescription
entryentry(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_positionexit_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.
adjustadjust(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

TypeSignature
WeightWeight(value: float)
QuantityQuantity(value: float)
NotionalNotional(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 leverage
  • Notional(n) + leverage=x → keeps exposure at n, while leverage affects margin / collateral usage
  • Quantity(q) + leverage=x → keeps position quantity at q, 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.

TypeSignatureDescription
PercentFromEntryPercentFromEntry(pct)Percent from entry price. -0.05 = 5% SL, 0.10 = 10% TP
ATRMultipleATRMultiple(multiple, period=14)Dynamic level at multiple × ATR from entry
AbsolutePriceAbsolutePrice(price)Fixed price level
TickOffsetFromEntryTickOffsetFromEntry(ticks)Tick offset from entry price
TrailingStopTrailingStop(trail_percent, activation_percent=0.0, initial_stop_percent=None)Percentage-point trailing stop (3 = 3%): tracks highest/lowest price and closes on pullback
TrailingATRTrailingATR(multiple, activation_percent=0.0, initial_stop_percent=None, period=14)ATR-based trailing; activation_percent and initial_stop_percent use percentage points
TakeProfitLevelSpecTakeProfitLevelSpec(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

ModeValueDescription
MARKETMarket orderDefault
LIMITLimit orderRequires limit_price
STOPStop-triggered market orderRequires trigger_price
STOP_LIMITStop-triggered limit orderRequires trigger_price and limit_price
PASSIVELimit + PostOnlyMaker only
IOCLimit + IOCImmediate or cancel
FOKLimit + FOKFill 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/FOK without limit_price -- Error
  • STOP without trigger_price -- Error
  • STOP_LIMIT without trigger_price or limit_price -- Error

Mutual exclusion:

  • take_profit + take_profits -- Error: use one or the other

Cross-dimension constraints:

  • PASSIVE + stop_loss -- Error: PostOnly contradicts trigger orders
  • PASSIVE + take_profit -- Error: PostOnly contradicts trigger orders
  • STOP/STOP_LIMIT + attached stop_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

ConditionDescription
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:

FunctionSignatureDescription
wallet_filledwallet_filled(address: str, coin: str = None)Whether tracked wallet has a fresh fill
wallet_sidewallet_side(address: str, coin: str = None)Latest fill side (BUY/SELL/NONE)
wallet_pricewallet_price(address: str, coin: str = None)Latest fill price
wallet_sizewallet_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.

FunctionSignatureDescription
tick_sizetick_size(ticker, price=None)Minimum price increment when exchange metadata is available.
step_sizestep_size(ticker)Minimum quantity increment.
min_qtymin_qty(ticker)Minimum order quantity when metadata exists, else None.
min_notionalmin_notional(ticker)Minimum notional when metadata exists, else None.
price_precisionprice_precision(ticker, price=None)Decimal places implied by price tick when available.
qty_precisionqty_precision(ticker)Decimal places implied by quantity step when available.
round_priceround_price(ticker, price, side=None, mode='exchange')Snap price to valid tick. exchange mode uses BUY/LONG=floor, SELL/SHORT=ceil.
round_qtyround_qty(ticker, qty, mode='down')Snap quantity to valid step size.
is_valid_priceis_valid_price(ticker, price)Whether price is already tick-aligned.
is_valid_qtyis_valid_qty(ticker, qty)Whether quantity is already step-aligned.
is_valid_notionalis_valid_notional(ticker, qty, price)Whether notional passes min-notional metadata check when available.
ticks_to_priceticks_to_price(ticker, ticks, price=None)Convert tick distance to absolute price distance.
price_to_ticksprice_to_ticks(ticker, price_diff, price=None)Convert absolute price distance to tick count.
qty_for_notionalqty_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.

FunctionSignatureDescription
rank_descrank_desc(scores)Rank tickers by descending score.
rank_ascrank_asc(scores)Rank tickers by ascending score.
top_n / bottom_ntop_n(scores, n) / bottom_n(scores, n)Select top/bottom N tickers.
top_pct / bottom_pcttop_pct(scores, pct) / bottom_pct(scores, pct)Select top/bottom percentile of tickers.
drop_nandrop_nan(scores)Remove non-finite scores.
cs_demeancs_demean(scores)Subtract cross-sectional mean.
cs_rankcs_rank(scores)Convert scores to percentile ranks.
cs_zscorecs_zscore(scores)Cross-sectional z-score normalize.
cs_clipcs_clip(scores, min_value, max_value)Clip scores to a bounded range.
equal_weightequal_weight(tickers, gross=1.0, side='LONG')Build equal-weight portfolio for a side.
long_short_equal_weightlong_short_equal_weight(longs, shorts, gross=1.0)Build market-neutral long/short equal weights.
weights_from_scoresweights_from_scores(scores, gross=1.0, demean=True)Convert scores to gross-normalized weights.
normalize_grossnormalize_gross(weights, gross=1.0)Scale weights to target gross exposure.
cap_weightscap_weights(weights, max_abs_weight)Cap per-asset absolute weight.
drop_small_weightsdrop_small_weights(weights, min_abs_weight)Remove tiny target weights.
merge_weightsmerge_weights(*weight_maps)Merge multiple weight maps by summing.
current_weightcurrent_weight(ticker)Estimated current weight for a ticker.
current_weightscurrent_weights(tickers=None)Estimated current weights map.
portfolio_valueportfolio_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_exposuregross_exposure()Estimated gross exposure.
net_exposurenet_exposure()Estimated net exposure.
free_balancefree_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_torebalance_to(weights, leverage=None, min_change=0.0, drop_missing_to_zero=True)Rebalance to target weights.
rebalance_market_neutralrebalance_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 (&, |, ~). Python and/or/not are 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.

FunctionSignatureReturnDescription
cancel_ordercancel_order(label: str)boolCancel pending order by label
has_open_orderhas_open_order(label: str)boolCheck pending order existence
get_open_ordersget_open_orders(ticker: str | None = None, side: str | None = None, prefix: str | None = None)list[dict]List pending/open orders
get_open_orderget_open_order(label: str)dict | NoneGet one pending/open order by label
replace_orderreplace_order(label: str, price: float | None = None, size: float | None = None, weight: float | None = None)boolCancel + recreate pending order
amend_orderamend_order(label: str, size: float | None = None, weight: float | None = None)boolIn-place size amendment (no price change)

Constraints:

  • Max 50 order-management calls per step
  • Label: max 64 chars, [a-zA-Z0-9_-]
  • size and weight are mutually exclusive

If runtime open-order state is available, get_open_orders() / get_open_order() may include:

  • account_id
  • order_id
  • label
  • asset
  • ticker
  • side
  • status
  • price
  • size
  • remaining_size
  • filled_size
  • fill_ratio
  • order_type
  • time_in_force
  • memo
  • created_at_ms
  • updated_at_ms
  • age_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

FieldTypeRequiredDescription
scriptstringYesSignal DSL script
exchangestringYesStrategy metadata / primary venue hint (not a ticker fallback)
universestring[]YesFull tickers to load and evaluate (EXCHANGE:BASE/QUOTE[:SETTLE])
timeframestringYesCandle: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M; live-only: tick
modestringNoisolated (default) or cross
leveragenumberNoOptional leverage value for perpetual strategies
strategy_namestringNoHuman-readable strategy name

10) Backtest vs Live Notes

FeatureBacktestLive
emit_ocoSupportedSupported
emit_chain + Filled(label)SupportedSupported
emit_chain + HedgeOnFill(label)Not modeledSupported in PAPER and LIVE deployment modes
cancel_order / replace_orderModeled by the strategy runtimeCapability-driven for the selected account and symbol
get_data(...).bookReturns NoneReturns live orderbook
wallet_* operatorsReturns safe defaultsReturns live wallet data (Hyperliquid, tick mode)
Order time in forceModeled by the backtest engineCapability-driven for the selected account and symbol
take_profits (scaled TP)Supported by the candle execution model (amount or pct)Supported (proportional partial fill)