Pitfalls

The mistakes that most often make a first draft fail validation, or pass it and then do the wrong thing. Check your code against this list before calling validate_agent_code.

Before you submit

  • No import, no wrapper function, no redeclared built-in name (RSI, News, Trade, manager, …).
  • Every indicator, getIntervalData and listener is created at the root; every getValue(), price read, order and notification is inside a callback.
  • Every symbol you trade or price from a cron is subscribed at the root (manager.onPreciseCandle(symbol, () => {}) or a candle series).
  • Intervals are minutes (60, 240, 1440) or 'week' / 'month'; daily and longer listeners pass the asset's time zone; schedules pass the zone of their wall-clock time.
  • Every value that can be missing is checked: hasValue() before getValue(), then === undefined on numbers and on each object field you use; getCurrentPrice can be undefined.
  • Indicator inputs match the signature: new RSI(candles.close, 14), not new RSI(candles, 14); Bollinger and RSI want a stream that is never undefined (candles.close), not another indicator's output. Check the Indicator reference.
  • Label values are compared with labels: Crossover gives 'firstUp' | 'secondUp' | null, not a number.
  • Orders: position: 'buy' | 'sell'; qty or amount; every amount has amountCurrency; LIMIT uses limitPrice (on placeOrder) or price (on openTrade).
  • Trades: positionSize is an object ({ type: 'proportion', proportion: 0.1 }); stop and take-profit values are prices, not distances; closeTrade(trade, { cause }) takes only cause and delta.
  • inhibitAutoInvest: true on orders that spend existing cash (sized from a capital reading, rebalances, sells); none on fixed contribution buys.
  • Rebalances trade the difference from target within one decremented cash budget.
  • getStatus<MyState>('default') with an interface whose fields are all optional; initialised before use.
  • getVariable('name', literal) called directly, once per name, at the root.
  • No Date.now(), new Date(), DateTime.now(), setTimeout, fetch, process, globalThis, as any, @ts-ignore.
  • Function parameters are typed; empty arrays are typed (const lines: string[] = []).
  • Only ES2020 library methods: no .at(), replaceAll, Object.hasOwn, findLast.
  • manager.llm on a schedule or candle claims a period key from getStatus first.
  • At most one sendNotification per callback run, and only if the user asked for alerts.

Behaviour traps that pass validation

A schedule never fires at launch. "Buy now, then weekly" needs a launch catch-up (see Acting at launch). A restart does not replay a missed occurrence either.

A schedule on a candle boundary runs before the candle closes. A '0 0 * * *' schedule reads yesterday's daily indicators. Use onIntervalCandle, or schedule a minute later.

Backtests start cold. Indicators are empty on the backtest start date: a 200-day average needs 200 days of the window before it has a value.

A condition that stays true fires every time. Without a latch in the status bag, "RSI below 30" re-alerts or re-buys on every candle it stays below 30.

Comparing the current value with a window that contains it. close > highest high of the last 20 candles is never true when the window includes the current candle. Exclude it with slice(0, -1).

Extreme and DC include the current candle, and DC is the channel's midline. For Donchian upper and lower bands, use History (or Extreme on the previous candle).

A missing value becomes 0. price ?? 0 or Number(undefined) turns "no data" into a price of zero, which satisfies every "below" condition. Skip the cycle instead.

Buying the whole target again at each rebalance. Portfolio value includes what you already hold; buying value × weight compounds exposure. Trade the difference.

Sizing a fixed contribution on free cash. "Buy $100 a week" sized on getAvailableCapital() buys all the idle cash the first time.

Sizing by getCurrentCapital(). It does not go down when you buy: it is not free cash. Use getAvailableCapital().

Deciding "am I in a position?" on the portfolio. getPositionQty(symbol) counts everything the portfolio holds, your own shares and other agents' included. A strategy gated on getPositionQty(symbol) === 0 never buys on a portfolio that already holds the symbol, and a sell of getPositionQty(symbol) sells shares the agent never bought. A backtest cannot show it: there the portfolio is the agent's alone. Test the agent's own trades, or its own fills (TS9047).

Exiting a position you did not open with an opposite order. Use closePosition(symbol): on a live account an opposite order can open a reverse position if the position changed meanwhile.

risk sizing without a stop. It then behaves like proportion, and a very tight stop asks for a very large position.

Trusting the status bag alone for orders. It is saved about every minute; after a crash the last changes can be lost. Also check getPositions() and pending orders before placing again.

handleError in a backtest fails the run. It is right for real failures (a data outage), not for expected conditions such as "no signal".

Ratios are fractions. A 20% margin from getPieuvreMetric is 0.20: margin > 20 never fires.

An unknown ranking universe returns nothing. universe: 'US' silently returns []; use a real tag such as 'us_equities' or 'sp500'.

Trading a ticker string. A calendar ticker, a model's answer or a name is not a trading key. Trade declared symbols, rank keys, resolveAssets keys or search_assets keys.

Showing internal keys to the user. Run-time trading keys are opaque identifiers. Name assets by ticker, name or your own symbol.

A US stock order at 09:00 New York. It is pre-market. Schedule at 09:31 or later, and gate catch-up schedules on the session.

Backtest or demo

A backtest replays history on a fresh paper wallet. These agents cannot be backtested and are validated in a demo run instead:

  • agents that read or manage the positions you already hold (a backtest wallet starts empty);
  • agents using searchNews, onNews, getPieuvreSeries, getPieuvreUpcoming or onPieuvreEvent;
  • agents calling manager.llm from a schedule or candle callback (a model call in a backtest only works inside onMessage on an X account);
  • agents following a Telegram channel, or an X handle that is not a literal;
  • agents with no declared asset whose universe does not come from getPieuvreRank;
  • alert-only agents that never trade.

getPieuvreRank and getPieuvreMetric universes, and X follows with a literal handle, are backtestable, point in time. A schedule that fires every minute makes long backtests too expensive: prefer hourly or coarser schedules for recurring work.

Also note that sendNotification, sendWebhook and comment do nothing in a backtest: use debugFlag and debugLog to see what a backtest did.

Browse the documentation