Place & Manage Orders

Orders are signed L2 transactions under client.tx. Each call signs locally with your API key, takes that key's next nonce, and submits. Needs a full client (Authenticated Setup).

The examples use market 0, mainnet's ETH perp. Trying them on testnet (Lighter.new(network='testnet')), use 4095, testnet's ETH perp: market ids differ per network (Networks).

Prices And Sizes Are Scaled Integers

Lighter signs integers, not decimals: a price is scaled by the market's supported_price_decimals, a size by its supported_size_decimals. client.scaler fetches both once and converts in both directions:

from decimal import Decimal

from typed_lighter import Lighter

async with Lighter.new() as client:
  eth = await client.scaler(0)
  print(eth.price(Decimal('2650.25')), eth.size(Decimal('0.05')))  # 265025 and 500
  print(eth.price_of(265025))  # Decimal('2650.25')

A price or size with more decimals than the market takes raises BadRequest rather than being rounded silently; pass rounding='floor', 'ceiling' or 'half-even' to round it. The market's min_base_amount and min_quote_amount apply to the unscaled values. See Numbers.

Place An Order

create_order takes one of seven order shapes, told apart by order_type (and, for a limit order, time_in_force); each only accepts its own fields (time_in_force only on limit orders, order_expiry only on resting ones, trigger_price only on stop-loss and take-profit orders). client_order_index is your own id for the order.

from decimal import Decimal

from typed_lighter import Lighter

async with Lighter.new() as client:
  eth = await client.scaler(0)
  await client.tx.create_order({
    'order_type': 'limit',
    'market_index': 0,
    'client_order_index': 1001,
    'base_amount': eth.size(Decimal('0.05')),  # 500
    'is_ask': False,
    'price': eth.price(Decimal('2000')),       # 200000
    'time_in_force': 'post-only',
  })

  await client.tx.create_order({
    'order_type': 'market',
    'market_index': 0,
    'client_order_index': 1002,
    'base_amount': 500,
    'is_ask': True,
    'price': 190_000,          # worst acceptable price
    'reduce_only': True,
  }, transport='ws')

  await client.tx.create_order({
    'order_type': 'stop-loss-limit',
    'market_index': 0,
    'client_order_index': 1003,
    'base_amount': 500,
    'is_ask': True,
    'trigger_price': 180_000,
    'price': 179_000,
    'reduce_only': True,
  })

transport='ws' sends the transaction over the WebSocket connection the client shares with its streams; 'http' (the default) posts it. The other order types are 'take-profit', 'take-profit-limit' and 'stop-loss'.

A resting limit order (LimitOrder) has a time_in_force of 'good-till-time' or 'post-only', and takes an optional order_expiry (5 minutes to 30 days out, 28 days by default). An immediate-or-cancel limit order (IocLimitOrder) fills what it can at the limit price or better and cancels the rest; it never rests, so it has no order_expiry field, and the type checker rejects one:

from typed_lighter import Lighter

async with Lighter.new() as client:
  await client.tx.create_order({
    'order_type': 'limit',
    'market_index': 0,
    'client_order_index': 1004,
    'base_amount': 500,
    'is_ask': False,
    'price': 200_000,
    'time_in_force': 'immediate-or-cancel',
  })

An untyped dict that still passes an order_expiry with an immediate-or-cancel order is refused with BadRequest before anything is signed.

Integrator Fees

An integrator (the app or bot operator placing orders for an account) can charge its own fee on an order, within caps the account first grants it with client.tx.approve_integrator. The client.tx order methods take no integrator: sign the order with its client.signer twin, which takes integrator= (on create_order, create_grouped_orders and modify_order), and submit it with client.tx.send:

from typed_lighter import Lighter

async with Lighter.new() as client:
  async with client.tx.reserve_nonces(1) as reservation:
    signed = client.signer.create_order(
      {
        'order_type': 'limit',
        'market_index': 0,
        'client_order_index': 1005,
        'base_amount': 500,
        'is_ask': False,
        'price': 200_000,
        'time_in_force': 'post-only',
      },
      integrator={'account_index': 12345, 'taker_fee': 500, 'maker_fee': 0},  # 1e-6 units: 0.05%
      nonce=reservation.nonces[0],
      api_key_index=reservation.api_key_index,
    )
    await client.tx.send(signed)

Fees are in 1e-6 units and must stay within the approved caps. Signing inside reserve_nonces takes the nonce from the client, as Batch Transactions explains.

Accepted Is Not Executed

Every transaction returns a SendTxResponse. A 200 means Lighter's API server accepted the signed transaction, not that it executed: the sequencer can still reject it, and a post-only order can still be cancelled for crossing. The outcome shows up in the account's orders:

from typed_lighter import Lighter

async with Lighter.new() as client:
  receipt = await client.tx.cancel_order(market_index=0, order_index=1001)
  print(receipt['tx_hash'], receipt['predicted_execution_time_ms'])

  active = await client.api.account.orders.active(account_index=client.signer.account_index, market_id=0)
  print([order['status'] for order in active['orders']])

For a push view, follow client.streams.account_all_orders (Listen To Streams).

Modify And Cancel

order_index accepts either Lighter's order_index or your client_order_index:

from typed_lighter import Lighter

async with Lighter.new() as client:
  await client.tx.modify_order(market_index=0, order_index=1001, base_amount=600, price=199_500)
  await client.tx.cancel_order(market_index=0, order_index=1001, transport='ws')

Cancel All

Three modes, one shape each:

from datetime import datetime, timedelta, timezone

from typed_lighter import Lighter

async with Lighter.new() as client:
  await client.tx.cancel_all_orders({'mode': 'immediate'})                   # every market
  await client.tx.cancel_all_orders({'mode': 'immediate', 'market_index': 0})  # one market

  deadline = datetime.now(timezone.utc) + timedelta(minutes=5)
  await client.tx.cancel_all_orders({'mode': 'scheduled', 'cancel_at': deadline})  # dead man's switch
  await client.tx.cancel_all_orders({'mode': 'abort'})                       # disarm it

Signing a new schedule before cancel_at pushes the deadline back.

Grouped Orders

OTO, OCO and OTOCO groups go in one transaction:

from typed_lighter import Lighter

async with Lighter.new() as client:
  await client.tx.create_grouped_orders({
    'grouping_type': 'otoco',
    'primary': {
      'order_type': 'limit', 'market_index': 0, 'base_amount': 500, 'is_ask': False,
      'price': 200_000, 'time_in_force': 'post-only',
    },
    'stop_loss': {'order_type': 'stop-loss', 'trigger_price': 190_000, 'price': 185_000},
    'take_profit': {'order_type': 'take-profit-limit', 'trigger_price': 220_000, 'price': 219_000},
  })

The stop-loss and take-profit are placed on the opposite side once the primary fills, sized by the fill, and cancel each other. An 'oto' group places one trigger order the same way, and an 'oco' group places a stop-loss and take-profit of one size and side straight away. Every stop-loss, take-profit and trigger in a group is reduce-only; there is no reduce_only field to set. The group's order_expiry applies to those orders and to a resting limit primary; an immediate-or-cancel primary has none.

Leverage And Margin

from typed_lighter import Lighter

async with Lighter.new() as client:
  await client.tx.update_leverage(market_index=0, initial_margin_fraction=1_000, margin_mode='isolated')  # 10x
  await client.tx.update_margin(market_index=0, usdc_amount=5_000_000, direction='add')  # 5 USDC

Upstream reference: Trading.