backtester
Original:🇺🇸 English
Translated
Use to perform market backtests with PlausibleAI Backtester, including symbol discovery, strategy validation, strategy mining, and batch execution.
9installs
Sourceserenorg/seren-skills
Added on
NPX Install
npx skill4agent add serenorg/seren-skills backtesterTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →PlausibleAI Backtester
How to Use This Skill
When this skill is active, use the guidance below directly. Do not perform filesystem searches or tool-driven exploration to rediscover it.
Use the PlausibleAI publisher API as the source of truth for symbol discovery, DSL discovery, validation, and execution. Prefer validating unfamiliar payloads before running them.
Base Route
All routes go through .
https://api.serendb.com/publishers/plausibleaiAuthentication
All endpoints require .
Authorization: Bearer $SEREN_API_KEYWorkflow
-
Resolve auth. Setfor bearer auth. Use
SEREN_API_KEYin examples; default it toSEREN_PUBLISHER_BASE_URL.https://api.serendb.com/publishers/plausibleai -
Discover the market universe before guessing symbols. Callto see supported market types and symbol counts. Call
GET /api/markets/typeswithGET /api/markets/symbols,market_type,search, andlimitwhen the symbol is unknown. Calloffsetwhen the caller needs metadata or data availability.GET /api/markets/symbols/{symbol} -
Load the DSL contract. Callbefore composing a new request shape. Treat the catalog as authoritative for indicators, parameters, operators, logic nodes, examples,
GET /api/backtests/catalog,price_adjustment_modes, and response metric definitions.entry_price_bases -
Build the request with stable rule ids. Every entry or exit rule must include a unique. Logic nodes reference rules by
id, never by position. Ifidis omitted, the API combines all rules in the set withlogic.AND -
Validate novel requests. Usewhen the request uses a new symbol, a new indicator combination, or a non-trivial logic tree. Surface validation errors directly instead of trying to guess what the API intended.
POST /api/backtests/validate -
Execute. Usefor a single run. Use
POST /api/backtestswhen the caller wants multiple independent runs. Batch requests run concurrently on the server; order in the response is stable regardless of completion order. Single-run backtests are also stored as short-lived retrievable results. The response body is a compact stored-result summary withPOST /api/backtests/batch,id, and follow-up links for fetching the full result, trades, and equity curve.expires_at -
Mine when the caller wants "the best actionable signal now". Use. Minimal request is just
POST /api/backtests/mine. Mining defaults to a sensible rolling window and ranks candidates by{ "symbol": "BTC-USD" }unless overridden. Mining returns a compact summary plus a nestedprofit_factorhandle withbacktest,id, and follow-up links.expires_at -
Retrieve large result sections incrementally. Usefor the stored full result. Use
GET /api/backtests/{id}for the full trade list, or addGET /api/backtests/{id}/tradeswhen pagination is needed. Use?limit=&offset=for the full equity curve, or addGET /api/backtests/{id}/equity-curvewhen pagination is needed. Stored results are ephemeral and expire automatically.?limit=&offset= -
Interpret the result carefully.is the summary.
reportis the buy-and-hold comparison over the same range.benchmarks.buy_and_holdshows the provider-native symbol actually used after the backend auto-resolves the best data source.execution.provider_symbolare closed trades. Each trade includestrades,trade_number,side,entry_bar_index,exit_bar_index,entry_date,exit_date,pnl, andduration_bars.exit_reasonis a snake_case string from a documented set:trades[].exit_reason,take_profit,stop_loss,trailing_stop,highest_high_exit,lowest_low_exit,exit_signal,end_of_data. The full list is inother.catalog.trade_exit_reasonsis trade-indexed, not bar-indexed, and uses the sameequity_curvevalues astrade_number. Top-leveltradesandfirst_entry_signal_atrefer to entry signals only.last_entry_signal_atreports signal counts and per-rule signal summaries using rule ids.diagnostics
Indicator Quick Reference
| Key | Category | Required Params | Optional Params / Notes |
|---|---|---|---|
| trend | | |
| trend | | |
| trend | | — |
| trend | | — |
| trend | | — |
| trend | | Returns +1 (uptrend) or -1 (downtrend); compare against 0 |
| momentum | | |
| momentum | | range 0–100 |
| momentum | | — |
| momentum | | — |
| momentum | | — |
| momentum | | — |
| momentum | | — |
| momentum | | — |
| momentum | | range: -100 to +100 |
| volatility | | source not accepted |
| volatility | | — |
| volatility | | — |
| volatility | | — |
| volatility | | |
| volatility | | — |
| volatility | | — |
| price_action | | |
| price_action | | |
| seasonal | — | Sun=0, Mon=1 … Fri=5, Sat=6; use |
| seasonal | — | 1–31 |
| seasonal | — | 1–5; resets on month change |
| seasonal | — | 1–12 |
| seasonal | — | 1–4 |
periodindicators[].parametersExecution Block Quick Reference
| Field | Type | Required | Notes |
|---|---|---|---|
| | yes | trade direction |
| enum | yes | |
| integer | no | used when any ATR-based entry offset or exit is present; defaults to |
| | no | only valid with |
Exit Policy Quick Reference
Price-based exits go in . A rule-based signal exit goes in .
exitsexit_signal| Field | Type | Mode options | Notes |
|---|---|---|---|
| | | value > 0 |
| | | value > 0 |
| | | value > 0 |
| integer | — | exits after N bars |
| integer | — | exits after N cumulative profitable closes since entry |
| integer | — | exits at the rolling highest high over N bars |
| integer | — | exits at the rolling lowest low over N bars |
| rule set | — | rule-based exit logic that can be combined with price exits |
If any ATR-based entry offset or exit is present and is omitted, the API defaults it to . is only valid with or .
execution.atr_period20entry_priceentry_mode: next_bar_limitnext_bar_stopExample Strategies
Use these as canonical request patterns for the main DSL surfaces.
1. Trend Following: 50/200 SMA Golden Cross
Good default example for rule ids and .
exit_signaljson
{
"symbol": "BTC-USD",
"timeframe": "daily",
"start_at": "2020-01-01",
"initial_capital": 100000,
"execution": {
"side": "long",
"entry_mode": "next_bar_open"
},
"entry": {
"rules": [
{
"id": "golden_cross",
"lhs": {
"indicator": {
"key": "sma",
"params": {
"period": 50,
"source": "close"
}
}
},
"operator": "crosses_above",
"rhs": {
"indicator": {
"key": "sma",
"params": {
"period": 200,
"source": "close"
}
}
}
}
],
"logic": {
"type": "rule",
"id": "golden_cross"
}
},
"exit_signal": {
"rules": [
{
"id": "death_cross",
"lhs": {
"indicator": {
"key": "sma",
"params": {
"period": 50,
"source": "close"
}
}
},
"operator": "crosses_below",
"rhs": {
"indicator": {
"key": "sma",
"params": {
"period": 200,
"source": "close"
}
}
}
}
],
"logic": {
"type": "rule",
"id": "death_cross"
}
}
}2. Mean Reversion: RSI Oversold Bounce
Good example for scalar thresholds plus and .
stop_losstake_profitjson
{
"symbol": "AAPL",
"timeframe": "daily",
"start_at": "2020-01-01",
"initial_capital": 100000,
"execution": {
"side": "long",
"entry_mode": "next_bar_open"
},
"entry": {
"rules": [
{
"id": "rsi_oversold",
"lhs": {
"indicator": {
"key": "rsi",
"params": {
"period": 14,
"source": "close"
}
}
},
"operator": "lte",
"rhs": {
"value": 30
}
}
],
"logic": {
"type": "rule",
"id": "rsi_oversold"
}
},
"exits": {
"stop_loss": {
"mode": "percent",
"value": 5
},
"take_profit": {
"mode": "percent",
"value": 10
},
"max_hold_bars": 20
}
}3. Trend Breakout: Stop Above 55-Bar High
Good example for a more canonical Donchian-style trend-following breakout with a long-term trend filter.
json
{
"symbol": "BTC-USD",
"timeframe": "daily",
"start_at": "2020-01-01",
"initial_capital": 100000,
"execution": {
"side": "long",
"entry_mode": "next_bar_stop",
"entry_price": {
"basis": "highest_high",
"lookback": 55,
"offset": {
"mode": "fixed",
"value": 0
}
}
},
"entry": {
"rules": [
{
"id": "above_sma_200",
"lhs": {
"field": "close"
},
"operator": "gte",
"rhs": {
"indicator": {
"key": "sma",
"params": {
"period": 200,
"source": "close"
}
}
}
}
],
"logic": {
"type": "rule",
"id": "above_sma_200"
}
},
"exits": {
"lowest_low_exit_lookback": 20
}
}Curl Reference
Use these snippets directly when you need to query or execute against the API.
Base Variables
bash
SEREN_PUBLISHER_BASE_URL="${SEREN_PUBLISHER_BASE_URL:-https://api.serendb.com/publishers/plausibleai}"
SEREN_API_KEY="${SEREN_API_KEY:?Set SEREN_API_KEY}"Every request uses:
bash
-H "Authorization: Bearer $SEREN_API_KEY"Market Discovery
List market types:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/markets/types" \
-H "Authorization: Bearer $SEREN_API_KEY" | jqSearch symbols:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/markets/symbols?market_type=crypto&search=bitcoin&limit=20" \
-H "Authorization: Bearer $SEREN_API_KEY" | jqGet symbol detail:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/markets/symbols/BTC-USD" \
-H "Authorization: Bearer $SEREN_API_KEY" | jqDSL Discovery
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/catalog" \
-H "Authorization: Bearer $SEREN_API_KEY" | jqValidate a Backtest
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/validate" \
-H "Authorization: Bearer $SEREN_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"symbol": "BTC-USD",
"timeframe": "daily",
"start_at": "2020-01-01",
"initial_capital": 100000,
"execution": {
"side": "long",
"entry_mode": "next_bar_open"
},
"entry": {
"rules": [
{
"id": "golden_cross",
"lhs": {
"indicator": {
"key": "sma",
"params": {
"period": 50,
"source": "close"
}
}
},
"operator": "crosses_above",
"rhs": {
"indicator": {
"key": "sma",
"params": {
"period": 200,
"source": "close"
}
}
}
}
],
"logic": {
"type": "rule",
"id": "golden_cross"
}
},
"exit_signal": {
"rules": [
{
"id": "death_cross",
"lhs": {
"indicator": {
"key": "sma",
"params": {
"period": 50,
"source": "close"
}
}
},
"operator": "crosses_below",
"rhs": {
"indicator": {
"key": "sma",
"params": {
"period": 200,
"source": "close"
}
}
}
}
],
"logic": {
"type": "rule",
"id": "death_cross"
}
}
}' | jqExecute a Backtest
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests" \
-H "Authorization: Bearer $SEREN_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"symbol": "BTC-USD",
"timeframe": "daily",
"start_at": "2020-01-01",
"initial_capital": 100000,
"execution": {
"side": "long",
"entry_mode": "next_bar_open"
},
"entry": {
"rules": [
{
"id": "golden_cross",
"lhs": {
"indicator": {
"key": "sma",
"params": {
"period": 50,
"source": "close"
}
}
},
"operator": "crosses_above",
"rhs": {
"indicator": {
"key": "sma",
"params": {
"period": 200,
"source": "close"
}
}
}
}
],
"logic": {
"type": "rule",
"id": "golden_cross"
}
},
"exit_signal": {
"rules": [
{
"id": "death_cross",
"lhs": {
"indicator": {
"key": "sma",
"params": {
"period": 50,
"source": "close"
}
}
},
"operator": "crosses_below",
"rhs": {
"indicator": {
"key": "sma",
"params": {
"period": 200,
"source": "close"
}
}
}
}
],
"logic": {
"type": "rule",
"id": "death_cross"
}
}
}' | jqThe response from is a compact stored-result summary. Use the returned or to fetch the full backtest result when needed.
POST /api/backtestsidlinks.full_result_pathRetrieve a Stored Backtest Result
Use the returned in the compact response from or .
idPOST /api/backtestsPOST /api/backtests/minebash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/<BACKTEST_ID>" \
-H "Authorization: Bearer $SEREN_API_KEY" | jqRetrieve all trades:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/<BACKTEST_ID>/trades" \
-H "Authorization: Bearer $SEREN_API_KEY" | jqRetrieve paginated trades when needed:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/<BACKTEST_ID>/trades?limit=100&offset=0" \
-H "Authorization: Bearer $SEREN_API_KEY" | jqRetrieve the full equity curve:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/<BACKTEST_ID>/equity-curve" \
-H "Authorization: Bearer $SEREN_API_KEY" | jqRetrieve paginated equity curve when needed:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/<BACKTEST_ID>/equity-curve?limit=100&offset=0" \
-H "Authorization: Bearer $SEREN_API_KEY" | jqMine an Actionable Strategy
Minimal mining request:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/mine" \
-H "Authorization: Bearer $SEREN_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"symbol": "BTC-USD"
}' | jqMining returns:
symbolsignal_atfitness_metricfitness_valuemined_candidatesactionable_candidatesrankbacktest
backtestidkindcreated_atexpires_atrequestsummarylinks.full_result_pathlinks.trades_pathlinks.equity_curve_path
Batch Execution
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/batch" \
-H "Authorization: Bearer $SEREN_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"requests": [
{
"symbol": "BTC-USD",
"timeframe": "daily",
"start_at": "2020-01-01",
"initial_capital": 100000,
"execution": {
"side": "long",
"entry_mode": "next_bar_open"
},
"entry": {
"rules": [
{
"id": "golden_cross",
"lhs": {
"indicator": {
"key": "sma",
"params": {
"period": 50,
"source": "close"
}
}
},
"operator": "crosses_above",
"rhs": {
"indicator": {
"key": "sma",
"params": {
"period": 200,
"source": "close"
}
}
}
}
],
"logic": {
"type": "rule",
"id": "golden_cross"
}
},
"exit_signal": {
"rules": [
{
"id": "death_cross",
"lhs": {
"indicator": {
"key": "sma",
"params": {
"period": 50,
"source": "close"
}
}
},
"operator": "crosses_below",
"rhs": {
"indicator": {
"key": "sma",
"params": {
"period": 200,
"source": "close"
}
}
}
}
],
"logic": {
"type": "rule",
"id": "death_cross"
}
}
}
]
}' | jqDSL Rules
- Keep rule ids short and stable. Use letters, numbers, underscores, or hyphens only.
- Use on
bars_agoorlhswhen you need to compare against an earlier bar. Omit it for the current bar.rhs - Use and
crosses_aboveonly when prior-bar behavior is intended.crosses_below - Prefer or
gteoverlteoreqfor floating-point comparisons.ne - Remember that uses parity semantics: it is true when an odd number of child nodes are true.
xor - is the maximum allowed
catalog.limits.max_bars_agovalue.bars_ago - The indicator only accepts
atr. Passingperiodtosourceis a validation error.atr - Indicator params are always required. The catalog lists no default value for them; omitting
periodreturns a 422.period - Add to any rule to invert its signal (fires when the condition is NOT met). Cannot be combined with
"negate": trueorcrosses_above— use the complementary operator instead. For compound negation, applycrosses_belowto individual rules and combine withnegate/anylogic nodes using De Morgan's laws.all
Common Pitfalls
| Mistake | Fix |
|---|---|
Omitting | Every rule in |
| Guessing indicator defaults | Always provide |
Passing | ATR does not accept source; use |
Using | Use |
| Cross operators need one additional prior bar beyond the indicator lookback |
| Not allowed — cross operators fire on a single bar; their negation fires on ~99% of bars. Use the complementary operator instead |
| Validation error — step must be ≤ max |
| Validation error — short must be < long |
| Both are valid simultaneously; |
| |
| Logic node referencing undefined id | Logic node |
| No signals firing | Check |
Response Discipline
- Prefer returning concise summaries unless the user asks for raw JSON.
- and
POST /api/backtestsboth return compact stored-result summaries first; do not assume the full backtest payload is in the initial response.POST /api/backtests/mine - For stored results, summarize the compact summary first and only fetch the full result, , or
tradeswhen the user needs that detail.equity_curve - Use for
?limit=&offset=ortradesonly when the result is large enough that incremental retrieval is useful.equity_curve - When showing example payloads, include rule ids explicitly.
- When the symbol is uncertain, use the market endpoints first instead of inventing tickers.
- When a backtest output looks surprising, compare ,
trades, andequity_curvebefore assuming the engine is wrong.diagnostics