PYX

Risk Limits & Sandbox

How Pyx protects your capital and isolates your scripts.

Risk limits

Every script task has configurable risk limits enforced in Rust, outside the Luau VM. Your script cannot bypass, modify, or inspect these limits - they're applied at the execution layer before orders reach the exchange.

Configuration

Set these when creating a task:

LimitTypeDescription
max_allocated_usdnumberMaximum total capital (price × size) across all open orders and positions. Orders that would exceed this are rejected.
max_open_ordersnumberMaximum total active orders across all tokens.
max_in_flightnumberMaximum orders that are submitted but not yet confirmed (pending).
max_in_flight_per_tokennumberSame as above, per token.
max_active_per_tokennumberMaximum active/ open orders per token.
max_position_per_tokennumberMaximum position size per token, in shares.
order_timeout_msnumberMilliseconds before a placed order is considered timed out and cleaned up. This includes partially filled orders.
min_ms_between_ordersnumberMinimum delay between order submissions. Prevents rapid-fire placement.

What happens when a limit is hit?

When pyx.buy() or pyx.sell() enqueues an order that would violate a limit:

  • The order is rejected before reaching the exchange
  • A warning is logged: "Order failed: [reason]"
  • Your script continues running - rejected orders don't crash the task
  • The spend/allocation tracking is unchanged (rejected orders don't reserve capital)

For a new strategy in testing:

{
  "max_allocated_usd": 100.0,
  "max_open_orders": 8,
  "max_in_flight": 4,
  "max_in_flight_per_token": 2,
  "max_active_per_token": 4,
  "max_position_per_token": 100.0,
  "order_timeout_ms": 30000,
  "min_ms_between_orders": 500
}

These are conservative. As you gain confidence in your strategy, you can widen them.


Sandbox

Your script runs in a sandboxed Luau VM with several layers of protection.

Available standard libraries

LibraryAvailableNotes
mathYesmath.floor, math.abs, math.max, math.random, etc.
stringYesstring.format, string.sub, string.find, etc.
tableYestable.insert, table.remove, table.sort, etc.
osNoRemoved entirely
ioNoRemoved entirely
debugNoRemoved entirely
package / requireNoRemoved entirely
dofile / loadfileNoRemoved entirely
load / string.dumpNoRemoved - prevents bytecode attacks

Memory limit

Each VM is capped at 8 MB of memory. If your script exceeds this (e.g., by building an unbounded table), the VM throws a memory error. Design your data structures with bounds:

-- Good: bounded history
table.insert(prices, value)
if #prices > 200 then
    table.remove(prices, 1)
end

-- Bad: unbounded growth
table.insert(all_prices_ever, value)  -- will eventually hit 8MB

If you need higher limits, please reach out to us.

Execution timeout

Callback timeouts are callback-specific:

  • on_price(ctx): 25ms timeout
  • on_spot_price(ctx): 25ms timeout
  • on_book(ctx): 25ms timeout
  • on_tick(ctx): 150ms timeout
  • on_fill(fill): 50ms timeout
  • on_placed(order): 50ms timeout
  • on_cancelled(order): 50ms timeout
  • on_split_complete(ctx): 50ms timeout
  • on_merge_complete(ctx): 50ms timeout

The timeout resets for each callback - a 20ms on_price followed by a 40ms on_fill is fine.

If a callback times out:

  • The current call is aborted
  • Any orders enqueued before the timeout are still submitted
  • The task usually continues running - but 5 Luau errors within 10 seconds will auto-stop the task with status error
  • The error is logged

Frozen globals

All standard library tables and the pyx module are read-only. You cannot:

-- These all fail silently or error
math.floor = nil
string.format = my_format
pyx.buy = function() end

Your global writes go to a local environment table. You can create and modify your own globals freely, but you can't tamper with builtins or the trading API.

No cross-task access

Each task has its own isolated VM. Task A cannot read task B's variables, positions, or orders. Even if they subscribe to the same markets, they're completely independent.

On this page