Skip to content

Agent skill for building a draft

You work on the script of a draft the user has open in the pnl editor. Your saved edits appear in their editor as you make them, and every run you start is paid from their compute credits. Listing and trading stay with the user.

Save this page as ~/.claude/skills/pnl-draft-script/SKILL.md (the raw file is https://docs.tradepnl.xyz/SKILLS.md). Endpoints are in the Backtest workspace API reference: drafts, the active script, the workspace, proposals, backtests, evaluations, markets and candles, and credits. The builder agent inside the app calls the same routes.

The user creates an API key under Agent CLI in the editor’s agent chat menu and gives it to you as PNL_API_KEY. Send it on every request:

Terminal window
API=https://mainnet.tradepnl.xyz # testnet: https://testnet.tradepnl.xyz
curl "$API/api/v1/drafts" -H "authorization: Bearer $PNL_API_KEY"

The API has its own hostname per network; the app hostnames (app., app-test.) serve the browser app only.

A drafts-scoped key only reads and edits (403 forbidden_scope on runs).

  1. Read. GET /api/v1/drafts/{id}/script returns the active script with revision, markets and diagnostics. Use the draft id the user gives you, or find it with GET /api/v1/drafts.
  2. Consult the reference when unsure. GET /api/v1/reference/script lists the language sections; GET /api/v1/reference/script/{section} returns one as markdown with samples. The same sections are static files: https://docs.tradepnl.xyz/reference/script/index.md (id, title, summary) and https://docs.tradepnl.xyz/reference/script/{section}.md. Read the index, then only the sections you need, rather than guessing.
  3. Edit with baseRevision. PATCH /api/v1/drafts/{id}/script with {"baseRevision": N, "edits": [{"find": "...", "replace": "..."}]} (each find must match exactly once) or {"baseRevision": N, "script": "..."} for a rewrite. The program is compiled first.
  4. Fix diagnostics. A program that does not compile is refused with 422 invalid_script and line-numbered diagnostics; nothing was saved. Fix those lines and send again.
  5. Backtest. POST /api/v1/backtests with {"draftId": "..."} queues the draft’s saved program on its venue and answers 202 {id}; then GET /api/v1/backtests/{id}?detail=summary until it stops answering 202. It is not validation and says nothing about passing the gate.
  6. Iterate in the workspace from what the result says about the idea: save variants as named scripts (PUT /api/v1/drafts/{id}/workspace/scripts/{name} with {"source": "...", "requireCompile": true}), backtest them by name ({"draftId": "...", "script": {"name": "..."}}), and keep notes (PUT …/workspace/notes/{name}). The user sees all of it on the Build page.
  7. Evaluate by name. POST /api/v1/evaluations/fast and POST /api/v1/evaluations/full take {"draftId", "script": {"name"}}; each verdict attaches to the version it evaluated. Both are queued: collect a fast evaluation from GET /api/v1/evaluations/fast/{id} as you collect a backtest.

Backtests and evaluations are already net of all trading costs; see After it compiles and the fee policy.

Check the balance with GET /api/v1/me/credits (displayCredits) and the prices with GET /api/v1/prices. A run the balance cannot cover is refused before it starts with 402 and {"error":"insufficient_credits","required":N,"balance":M,"topUp":"/credits"}: stop and tell the user how many credits the next step needs. A run that fails is not charged.

  • Keep full results on disk, not in your context. Make one cache directory per draft (for example ./.pnl-cache/{draftId}/). Run variants with POST /api/v1/backtests and an inline {"script": {"source", "name"}, "venue"}, collect each with GET /api/v1/backtests/{id}?detail=full (the net equity curve, the window, per-market daily attribution; gzipped when you send accept-encoding: gzip), write each response to a file named by the sha256 of the script you sent, and keep only a one-line summary per run in your context. Analyse the files with your own tools.
  • Preview the cost of a search first. POST /api/v1/trials/preview with {"script": {...}, "planned": N} returns the family’s observed trials, its trial floor, the effective count now and after N more runs, and in words how much smaller a p-value would then have to be. Show that to the user before a search, and ask them when it multiplies the count.
  • Search with ask/tell, locally. Use an optimiser with an ask/tell interface (optuna’s study.ask() / study.tell(), TPE or random) on your machine: ask for a parameter set, write it into the script’s param lines, backtest it through the same endpoint, and tell the study the metric you chose. Every call is a trial counted exactly like the builder agent’s runs, and costs credits.
  • Discipline. Optimise only a stated hypothesis over its declared params and bounds, with a small budget (30 or fewer unless the user agrees), and report how the best trial’s neighbours did. The best of N trials is biased upward: never present it as validated. A sharp optimum is usually noise; fast evaluation’s parameter perturbations show it.

How to research a strategy:

  1. Explore cheaply with backtests and studies.
  2. Check promising candidates periodically with a fast evaluation. It is advisory feedback, not the gate.
  3. Once a candidate is solid, iterate with full evaluations. The full evaluation is the real gate and part of the feedback loop, not a one-off final step: read its verdict, improve the candidate, evaluate again.
  4. Every run (backtest, fast evaluation or full evaluation) is a trial of the strategy’s family and costs credits at the live price book.

Fast evaluation is POST /api/v1/evaluations/fast, collected from GET /api/v1/evaluations/fast/{id}; full evaluation is POST /api/v1/evaluations/full (check …/availability first) and GET /api/v1/evaluations/full/{jobId} for its verdict. A full evaluation costs far more than a backtest: ask the user before you start one.

  • 409 means re-read. 409 stale_revision means the user or the builder agent edited the draft since your read. Read it again and redo your change on the new script; never resend the old one.
  • Every backtest is a trial. Each one is counted against the user’s wallet, and the evaluation gate gets stricter with every configuration tried. Search only as above, and think between runs. A strategy that needs one exact constant does not work.
  • Listing and trading stay with the user, signed in to the app.
  • On 429 (or 503 queue_overloaded), wait for retry-after / retryAfterMs before trying again. At most two of your backtests may be queued or running at once (429 too_many_backtests).

The network backtest queue holds 250 queued or running jobs. Fast backtests get 60 seconds to reach their first worker, then 30 seconds for loading and execution. queue_timeout and execution_timeout fail without a charge or successful trial. A pending collection (queued or running) means poll the same job ID again; a collection wait ending is not a reason to submit another job.