Skip to main content
Order execution handles the placement of market orders on the Lighter exchange (live mode) or against simulated order books (simulation mode), with robust fill tracking to verify actual execution prices and quantities.

Execution Flow

1

Order Placement

Create order on exchange or simulator with market order type
2

Transaction Tracking

Poll transaction status until executed, failed, or rejected
3

Fill Verification

Query order status API to get actual filled quantity and average price
4

Database Persistence

Save position to Orders table with real fill details
5

SL/TP Placement

Place stop-loss and take-profit orders if specified in exit plan

Live Order Execution

Creating Orders

Orders are placed via the Lighter SignerClient with Immediate-Or-Cancel (IOC) time-in-force.
async function executeOrderOnExchange(
  client: SignerClient,
  params: {
    market: MarketMetadata;
    side: "LONG" | "SHORT";
    quantity: number;
    latestPrice: number;
    accountIndex: number;
  },
): Promise<ExecuteOrderResult> {
  const { market, side, quantity, latestPrice, accountIndex } = params;
  const directionIsLong = side === "LONG";

  // Create market order with 1% price buffer for slippage
  const [, txHash, orderError] = await client.createOrder({
    marketIndex: market.marketId,
    clientOrderIndex: market.clientOrderIndex,
    baseAmount: Math.round(quantity * market.qtyDecimals),
    price: Math.round(
      (directionIsLong ? latestPrice * 1.01 : latestPrice * 0.99) * market.priceDecimals,
    ),
    isAsk: !directionIsLong, // LONG = buy (isAsk=false), SHORT = sell (isAsk=true)
    orderType: SignerClient.ORDER_TYPE_MARKET,
    timeInForce: SignerClient.ORDER_TIME_IN_FORCE_IMMEDIATE_OR_CANCEL,
    reduceOnly: false,
    triggerPrice: SignerClient.NIL_TRIGGER_PRICE,
    orderExpiry: SignerClient.DEFAULT_IOC_EXPIRY,
  });

  if (orderError) {
    console.error(`[createPosition] Order creation failed:`, orderError);
    return {
      success: false,
      filledQuantity: 0,
      actualEntryPrice: 0,
      error: `Order creation failed: ${orderError}`,
    };
  }

  // Track fill with polling
  const fillResult = await trackFill(client, {
    txHash,
    accountIndex,
    marketId: market.marketId,
    clientOrderIndex: market.clientOrderIndex,
    requestedQuantity: quantity,
    maxWaitMs: 30_000,
    pollIntervalMs: 500,
  });

  if (!fillResult.success || !fillResult.filled) {
    console.error(`[createPosition] Order not filled:`, fillResult.error);
    return {
      success: false,
      filledQuantity: 0,
      actualEntryPrice: 0,
      error: fillResult.error ?? "Order not filled",
    };
  }

  const filledQuantity = fillResult.filledQuantity;
  const actualEntryPrice = fillResult.averagePrice > 0 ? fillResult.averagePrice : latestPrice;

  if (fillResult.partialFill) {
    console.warn(
      `[createPosition] Partial fill: requested=${quantity}, filled=${filledQuantity}`,
    );
  }

  console.log(`[createPosition] Fill confirmed: qty=${filledQuantity}, price=${actualEntryPrice}`);

  return {
    success: true,
    filledQuantity,
    actualEntryPrice,
  };
}
Location: src/server/features/trading/createPosition.ts:456 Key Parameters:
  • baseAmount: Quantity scaled by market decimals (e.g., 1.5 BTC → 150000000)
  • price: Limit price with 1% buffer for market orders (prevents rejection)
  • isAsk: false for LONG (buy), true for SHORT (sell)
  • orderType: ORDER_TYPE_MARKET (0) for immediate execution
  • timeInForce: IMMEDIATE_OR_CANCEL fills immediately or cancels unfilled portion

Fill Tracking

The trackFill function uses exponential backoff polling to verify order execution and extract actual fill details.
export async function trackFill(
  client: LighterSignerClient,
  params: TrackFillParams,
): Promise<FillResult> {
  const {
    txHash,
    accountIndex,
    marketId,
    clientOrderIndex,
    requestedQuantity,
    maxWaitMs = 30_000,
    pollIntervalMs = 500,
  } = params;

  const startTime = Date.now();
  let transaction: any = null;

  // Step 1: Wait for transaction to be processed
  try {
    transaction = await client.waitForTransaction(txHash, maxWaitMs, pollIntervalMs);
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    console.error(`[FillTracker] waitForTransaction failed for ${txHash}:`, message);
    return {
      success: false,
      filled: false,
      filledQuantity: 0,
      averagePrice: 0,
      partialFill: false,
      error: `Transaction tracking failed: ${message}`,
      txStatus: "pending",
    };
  }

  // Check transaction status
  const txStatus = transaction?.status ?? transaction?.tx_status;
  if (txStatus === TX_STATUS.FAILED) {
    return {
      success: false,
      filled: false,
      filledQuantity: 0,
      averagePrice: 0,
      partialFill: false,
      error: "Transaction failed on chain",
      txStatus: "failed",
    };
  }

  if (txStatus !== TX_STATUS.EXECUTED) {
    return {
      success: false,
      filled: false,
      filledQuantity: 0,
      averagePrice: 0,
      partialFill: false,
      error: `Unexpected transaction status: ${txStatus}`,
      txStatus: "pending",
    };
  }

  // Step 2: Check order status with exponential backoff
  const apiClient = new ApiClient({ host: BASE_URL });
  const orderApi = (apiClient as any).orders ?? null;

  let authToken: string | undefined;
  try {
    authToken = await client.createAuthToken();
  } catch (err) {
    console.warn("[FillTracker] Failed to create auth token, proceeding without:", err);
  }

  let orderStatus: OrderStatusResult | undefined;
  const remainingTime = maxWaitMs - (Date.now() - startTime);

  if (remainingTime > 0 && orderApi) {
    try {
      orderStatus = await checkOrderStatus(
        orderApi,
        accountIndex,
        marketId,
        clientOrderIndex,
        authToken,
        Math.ceil(remainingTime / 1000),
      );
    } catch (err) {
      console.warn("[FillTracker] checkOrderStatus failed:", err);
    }
  }

  // Parse fill details from order status
  if (orderStatus?.found) {
    const filledAmount = parseFloat(orderStatus.filledAmount ?? "0");
    const order = orderStatus.order;
    const avgPrice = order?.price != null ? parseFloat(order.price) : 0;
    const partialFill = filledAmount > 0 && filledAmount < requestedQuantity;

    await apiClient.close();

    return {
      success: true,
      filled: filledAmount > 0,
      filledQuantity: filledAmount,
      averagePrice: avgPrice,
      partialFill,
      txStatus: "executed",
      orderStatus,
    };
  }

  // Fallback: If order status check failed, try fetching account trades
  const accountApi = new AccountApi(apiClient);
  let fillResult: FillResult | null = null;

  try {
    fillResult = await fetchRecentFills(
      accountApi,
      accountIndex,
      marketId,
      requestedQuantity,
      startTime,
    );
  } catch (err) {
    console.warn("[FillTracker] fetchRecentFills failed:", err);
  }

  await apiClient.close();

  if (fillResult) {
    return fillResult;
  }

  // Conservative fallback: assume full fill for market orders
  return {
    success: true,
    filled: true,
    filledQuantity: requestedQuantity,
    averagePrice: 0, // Unknown - caller should use last price
    partialFill: false,
    txStatus: "executed",
    error: "Could not verify fill details, assumed full fill",
  };
}
Location: src/server/features/trading/fillTracker.ts:72 Transaction Status Codes:
const TX_STATUS = {
  PENDING: 0,
  QUEUED: 1,
  COMMITTED: 2,
  EXECUTED: 3,  // Success
  FAILED: 4,     // Transaction failed
  REJECTED: 5,   // Transaction rejected
} as const;
Fill Verification Steps:
  1. waitForTransaction(): Poll transaction until status is EXECUTED (3)
  2. checkOrderStatus(): Query OrderAPI for fill amount and average price
  3. Fallback: If order status unavailable, fetch recent trades from AccountAPI
  4. Conservative assumption: If all else fails, assume full fill (logged as warning)
The fallback assumes full fill to prevent stuck workflows, but logs a warning. In production, you should monitor these cases and investigate API issues.

Partial Fills

Partial fills occur when order book depth is insufficient to fill the entire order.
const filledAmount = parseFloat(orderStatus.filledAmount ?? "0");
const partialFill = filledAmount > 0 && filledAmount < requestedQuantity;

return {
  success: true,
  filled: filledAmount > 0,
  filledQuantity: filledAmount,
  averagePrice: avgPrice,
  partialFill,
  txStatus: "executed",
  orderStatus,
};
Handling:
  • System accepts partial fills and creates position with actual filled quantity
  • Logs warning: Partial fill: requested=X, filled=Y
  • Database stores actual filled quantity, not requested quantity

Simulated Execution

The simulator matches orders against cached order book snapshots using a realistic matching engine.
function asTaker(ctx: MatchingContext): OrderExecution {
  const { order, book } = ctx;
  const levels = order.side === "buy" ? book.asks : book.bids;

  if (!levels || levels.length === 0) {
    return {
      fills: [],
      averagePrice: 0,
      totalQuantity: 0,
      totalFees: 0,
      status: "rejected",
      reason: "no liquidity available",
    };
  }

  let remaining = order.quantity;
  const fills: FillDetail[] = [];
  let totalNotional = 0;

  // Walk the order book levels
  for (const level of levels) {
    if (remaining <= 0) break;
    const executable = Math.min(remaining, level.quantity);
    if (executable <= 0) continue;

    const price = level.price;

    fills.push({
      quantity: executable,
      price,
    });
    remaining -= executable;
    totalNotional += price * executable;
  }

  if (fills.length === 0) {
    return {
      fills,
      averagePrice: 0,
      totalQuantity: 0,
      totalFees: 0,
      status: "rejected",
      reason: "insufficient liquidity",
    };
  }

  const totalQuantity = fills.reduce((sum, fill) => sum + fill.quantity, 0);
  const averagePrice = totalNotional / totalQuantity;
  const status = remaining > 0 ? "partial" : "filled";

  return {
    fills,
    averagePrice,
    totalQuantity,
    totalFees: 0,
    status,
    reason: status === "partial" ? "insufficient book depth" : undefined,
  };
}
Location: src/server/features/simulator/orderMatching.ts:21 Matching Algorithm:
  1. Identify aggressing side (buy → asks, sell → bids)
  2. Walk order book levels from best price
  3. Fill against each level until quantity exhausted or book empty
  4. Calculate weighted average price from all fills
  5. Return partial fill if remaining quantity > 0
Realism Features:
  • Actual order book depth from Lighter API
  • Partial fills when liquidity insufficient
  • Volume-weighted average price calculation
  • Slippage modeling through multi-level fills

Position Scaling

When opening a position for a symbol with an existing OPEN position of the same side, the system scales into the position rather than creating a new one.
const existingOrder = await getOpenOrderBySymbol(modelId, symbol.toUpperCase());

if (existingOrder && existingOrder.side === side) {
  // Scale into existing position
  const exitPlan = buildExitPlan(
    stopLoss,
    profitTarget,
    invalidationCondition,
    confidence,
    existingOrder.exitPlan as ExitPlan | null,
  );

  const scaleResult = await scaleIntoExistingOrder({
    existingOrder,
    newQuantity: filledQuantity,
    newEntryPrice: entryPrice,
    exitPlan,
  });

  return {
    orderId: scaleResult.order.id,
    quantity: scaleResult.totalQuantity,
    entryPrice: scaleResult.avgEntryPrice,
    isScaleIn: true,
    existingOrder,
  };
}
Location: src/server/features/trading/createPosition.ts:569 Weighted Average Entry Price:
function calculateWeightedAvgEntry(
  prevQuantity: number,
  prevEntryPrice: number,
  newQuantity: number,
  newEntryPrice: number,
): number {
  const prevNotional = prevEntryPrice * prevQuantity;
  const newNotional = newEntryPrice * newQuantity;
  const totalQty = prevQuantity + newQuantity;
  return totalQty !== 0 ? (prevNotional + newNotional) / totalQty : newEntryPrice;
}
Example:
  • Existing position: 1.0 BTC @ $50,000
  • New execution: 0.5 BTC @ $51,000
  • Result: 1.5 BTC @ $50,333 (weighted average)
When scaling in, SL/TP orders are cancelled and replaced with new orders reflecting the updated quantity and average entry price.

Closing Positions

Closing a position places a reverse order (LONG → sell, SHORT → buy) with reduceOnly: true.
const closeSign = position.sign === "LONG" ? "SHORT" : "LONG";
const baseQuantity = position.quantity ?? toNumber(position.position) ?? 0;

// Cancel any pending SL/TP orders before closing position
const dbOrderForSlTp = await getOpenOrderBySymbol(account.id, key);
if (dbOrderForSlTp?.slOrderIndex || dbOrderForSlTp?.tpOrderIndex) {
  console.log(`[closePosition] Canceling SL/TP orders for ${symbol}`);
  await cancelSlTpOrders(
    client,
    symbol.toUpperCase(),
    dbOrderForSlTp.slOrderIndex,
    dbOrderForSlTp.tpOrderIndex,
    dbOrderForSlTp.id,
  );
}

// Execute close order
const [orderInfo, txHash, orderError] = await client.createOrder({
  marketIndex: market.marketId,
  clientOrderIndex: market.clientOrderIndex,
  baseAmount: Math.abs(baseQuantity) * market.qtyDecimals,
  price:
    (closeSign === "LONG" ? latestPrice * 1.01 : latestPrice * 0.99) *
    market.priceDecimals,
  isAsk: closeSign !== "LONG",
  orderType: SignerClient.ORDER_TYPE_MARKET,
  timeInForce: SignerClient.ORDER_TIME_IN_FORCE_IMMEDIATE_OR_CANCEL,
  reduceOnly: true, // Ensure order only reduces position, never increases
  triggerPrice: SignerClient.NIL_TRIGGER_PRICE,
  orderExpiry: SignerClient.DEFAULT_IOC_EXPIRY,
});

// Track fill to get actual exit price
const fillResult = await trackFill(client, {
  txHash,
  accountIndex: Number(account.accountIndex),
  marketId: market.marketId,
  clientOrderIndex: market.clientOrderIndex,
  requestedQuantity: Math.abs(baseQuantity),
  maxWaitMs: 30_000,
  pollIntervalMs: 500,
});

// Use actual fill price if available, otherwise fall back to latest price
const actualExitPrice = fillResult.success && fillResult.averagePrice > 0
  ? fillResult.averagePrice
  : latestPrice;

// Update order in database with actual fill price
const dbOrder = await getOpenOrderBySymbol(account.id, key);
if (dbOrder) {
  await closeOrder({
    orderId: dbOrder.id,
    exitPrice: actualExitPrice.toString(),
    realizedPnl: (summary.netPnl ?? 0).toString(),
  });
}
Location: src/server/features/trading/closePosition.ts:226 Close Flow:
  1. Cancel any existing SL/TP orders
  2. Place reverse market order with reduceOnly: true
  3. Track fill to get actual exit price
  4. Calculate realized P&L
  5. Update Orders table: set status=CLOSED, exitPrice, realizedPnl, closedAt

Error Handling

Causes:
  • Invalid market parameters
  • Insufficient account balance
  • Exchange connectivity issues
Handling:
  • Return error in PositionResult
  • Log error message
  • Do not persist to database
  • Agent receives error and can retry with different parameters
Causes:
  • Network congestion
  • Exchange processing delays
  • Transaction stuck in pending state
Handling:
  • waitForTransaction() times out after 30 seconds
  • Return error: “Transaction tracking failed: timeout”
  • Position creation aborted
  • Agent can retry in next invocation
Causes:
  • Order API unavailable
  • Order not indexed yet
  • Authentication token issues
Handling:
  • Try fallback: fetch recent trades from AccountAPI
  • If fallback fails: assume full fill (logged as warning)
  • Position persisted with conservative assumption
  • Monitor logs for frequent fallback usage
Causes:
  • Insufficient order book depth
  • Large order size relative to liquidity
Handling:
  • Accept partial fill
  • Create position with actual filled quantity
  • Log warning for monitoring
  • Database stores actual quantity, not requested

Best Practices

Always Track Fills

Never assume order execution succeeded. Always use trackFill() to verify actual filled quantity and average price.

Handle Partial Fills

Design your system to gracefully handle partial fills. Store actual filled quantities, not requested amounts.

Use Reduce-Only for Closes

Always set reduceOnly: true when closing positions to prevent accidental position flips.

Cancel SL/TP Before Close

Always cancel existing SL/TP orders before manually closing a position to prevent race conditions.

Next Steps

Position Management

Learn about position tracking and exit plans

Risk Controls

Explore risk management and position sizing

Build docs developers (and LLMs) love