backtester

Original🇺🇸 English
Translated

Use to perform market backtests with PlausibleAI Backtester, including symbol discovery, strategy validation, strategy mining, and batch execution.

9installs
Added on

NPX Install

npx skill4agent add serenorg/seren-skills backtester

Tags

Translated version includes tags in frontmatter

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/plausibleai
.

Authentication

All endpoints require
Authorization: Bearer $SEREN_API_KEY
.

Workflow

  1. Resolve auth. Set
    SEREN_API_KEY
    for bearer auth. Use
    SEREN_PUBLISHER_BASE_URL
    in examples; default it to
    https://api.serendb.com/publishers/plausibleai
    .
  2. Discover the market universe before guessing symbols. Call
    GET /api/markets/types
    to see supported market types and symbol counts. Call
    GET /api/markets/symbols
    with
    market_type
    ,
    search
    ,
    limit
    , and
    offset
    when the symbol is unknown. Call
    GET /api/markets/symbols/{symbol}
    when the caller needs metadata or data availability.
  3. Load the DSL contract. Call
    GET /api/backtests/catalog
    before composing a new request shape. Treat the catalog as authoritative for indicators, parameters, operators, logic nodes, examples,
    price_adjustment_modes
    ,
    entry_price_bases
    , and response metric definitions.
  4. Build the request with stable rule ids. Every entry or exit rule must include a unique
    id
    . Logic nodes reference rules by
    id
    , never by position. If
    logic
    is omitted, the API combines all rules in the set with
    AND
    .
  5. Validate novel requests. Use
    POST /api/backtests/validate
    when 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.
  6. Execute. Use
    POST /api/backtests
    for a single run. Use
    POST /api/backtests/batch
    when 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 with
    id
    ,
    expires_at
    , and follow-up links for fetching the full result, trades, and equity curve.
  7. Mine when the caller wants "the best actionable signal now". Use
    POST /api/backtests/mine
    . Minimal request is just
    { "symbol": "BTC-USD" }
    . Mining defaults to a sensible rolling window and ranks candidates by
    profit_factor
    unless overridden. Mining returns a compact summary plus a nested
    backtest
    handle with
    id
    ,
    expires_at
    , and follow-up links.
  8. Retrieve large result sections incrementally. Use
    GET /api/backtests/{id}
    for the stored full result. Use
    GET /api/backtests/{id}/trades
    for the full trade list, or add
    ?limit=&offset=
    when pagination is needed. Use
    GET /api/backtests/{id}/equity-curve
    for the full equity curve, or add
    ?limit=&offset=
    when pagination is needed. Stored results are ephemeral and expire automatically.
  9. Interpret the result carefully.
    report
    is the summary.
    benchmarks.buy_and_hold
    is the buy-and-hold comparison over the same range.
    execution.provider_symbol
    shows the provider-native symbol actually used after the backend auto-resolves the best data source.
    trades
    are closed trades. Each trade includes
    trade_number
    ,
    side
    ,
    entry_bar_index
    ,
    exit_bar_index
    ,
    entry_date
    ,
    exit_date
    ,
    pnl
    ,
    duration_bars
    , and
    exit_reason
    .
    trades[].exit_reason
    is a snake_case string from a documented set:
    take_profit
    ,
    stop_loss
    ,
    trailing_stop
    ,
    highest_high_exit
    ,
    lowest_low_exit
    ,
    exit_signal
    ,
    end_of_data
    ,
    other
    . The full list is in
    catalog.trade_exit_reasons
    .
    equity_curve
    is trade-indexed, not bar-indexed, and uses the same
    trade_number
    values as
    trades
    . Top-level
    first_entry_signal_at
    and
    last_entry_signal_at
    refer to entry signals only.
    diagnostics
    reports signal counts and per-rule signal summaries using rule ids.

Indicator Quick Reference

KeyCategoryRequired ParamsOptional Params / Notes
sma
trend
period
(int)
source
(default:
close
)
ema
trend
period
(int)
source
(default:
close
)
adx
trend
period
(int)
positive_directional_indicator
trend
period
(int)
negative_directional_indicator
trend
period
(int)
parabolic_sar
trend
af_step
(float, e.g. 0.02),
af_max
(float, e.g. 0.20)
Returns +1 (uptrend) or -1 (downtrend); compare against 0
rsi
momentum
period
(int)
source
(default:
close
)
stochastic_oscillator
momentum
period
(int)
range 0–100
momentum
momentum
period
(int)
cci
momentum
period
(int)
roc
momentum
period
(int)
macd_line
momentum
fast
,
slow
,
signal
(all int, fast < slow)
macd_signal_line
momentum
fast
,
slow
,
signal
(all int, fast < slow)
macd_histogram
momentum
fast
,
slow
,
signal
(all int, fast < slow)
tsi
momentum
long_period
(int),
short_period
(int, must be < long_period)
range: -100 to +100
atr
volatility
period
(int)
source not accepted
atr_percent
volatility
period
(int)
bollinger_upper_band
volatility
period
,
num_std
bollinger_lower_band
volatility
period
,
num_std
standard_deviation
volatility
period
(int)
source
(default:
close
)
keltner_upper_band
volatility
period
(int),
multiplier
(float)
keltner_lower_band
volatility
period
(int),
multiplier
(float)
highest
price_action
period
(int)
source
(default:
high
)
lowest
price_action
period
(int)
source
(default:
low
)
day_of_week
seasonalSun=0, Mon=1 … Fri=5, Sat=6; use
eq
to target a specific day
day_of_month
seasonal1–31
week_of_month
seasonal1–5; resets on month change
month
seasonal1–12
quarter
seasonal1–4
period
is always required — the server never defaults it. The catalog
indicators[].parameters
array is the source of truth.

Execution Block Quick Reference

FieldTypeRequiredNotes
side
"long"
|
"short"
yestrade direction
entry_mode
enumyes
this_bar_close
,
next_bar_open
(most common),
next_bar_limit
,
next_bar_stop
atr_period
integernoused when any ATR-based entry offset or exit is present; defaults to
20
when omitted
entry_price
{basis, lookback, offset}
noonly valid with
next_bar_limit
or
next_bar_stop
; bases:
none
,
highest_high
,
lowest_low
;
offset
is
{mode, value}
and moves the reference price up or down

Exit Policy Quick Reference

Price-based exits go in
exits
. A rule-based signal exit goes in
exit_signal
.
FieldTypeMode optionsNotes
exits.stop_loss
{mode, value}
fixed
,
percent
,
atr
value > 0
exits.take_profit
{mode, value}
fixed
,
percent
,
atr
value > 0
exits.trailing_stop
{mode, value}
fixed
,
percent
,
atr
value > 0
exits.max_hold_bars
integerexits after N bars
exits.profitable_closes
integerexits after N cumulative profitable closes since entry
exits.highest_high_exit_lookback
integerexits at the rolling highest high over N bars
exits.lowest_low_exit_lookback
integerexits at the rolling lowest low over N bars
exit_signal
rule setrule-based exit logic that can be combined with price exits
If any ATR-based entry offset or exit is present and
execution.atr_period
is omitted, the API defaults it to
20
.
entry_price
is only valid with
entry_mode: next_bar_limit
or
next_bar_stop
.

Example 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_signal
.
json
{
  "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
stop_loss
and
take_profit
.
json
{
  "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" | jq
Search symbols:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/markets/symbols?market_type=crypto&search=bitcoin&limit=20" \
  -H "Authorization: Bearer $SEREN_API_KEY" | jq
Get symbol detail:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/markets/symbols/BTC-USD" \
  -H "Authorization: Bearer $SEREN_API_KEY" | jq

DSL Discovery

bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/catalog" \
  -H "Authorization: Bearer $SEREN_API_KEY" | jq

Validate 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"
      }
    }
  }' | jq

Execute 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"
      }
    }
  }' | jq
The response from
POST /api/backtests
is a compact stored-result summary. Use the returned
id
or
links.full_result_path
to fetch the full backtest result when needed.

Retrieve a Stored Backtest Result

Use the
id
returned in the compact response from
POST /api/backtests
or
POST /api/backtests/mine
.
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/<BACKTEST_ID>" \
  -H "Authorization: Bearer $SEREN_API_KEY" | jq
Retrieve all trades:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/<BACKTEST_ID>/trades" \
  -H "Authorization: Bearer $SEREN_API_KEY" | jq
Retrieve 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" | jq
Retrieve the full equity curve:
bash
curl -sS "$SEREN_PUBLISHER_BASE_URL/api/backtests/<BACKTEST_ID>/equity-curve" \
  -H "Authorization: Bearer $SEREN_API_KEY" | jq
Retrieve 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" | jq

Mine 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"
  }' | jq
Mining returns:
  • symbol
  • signal_at
  • fitness_metric
  • fitness_value
  • mined_candidates
  • actionable_candidates
  • rank
  • backtest
backtest
contains the stored result handle and compact summary:
  • id
  • kind
  • created_at
  • expires_at
  • request
  • summary
  • links.full_result_path
  • links.trades_path
  • links.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"
          }
        }
      }
    ]
  }' | jq

DSL Rules

  • Keep rule ids short and stable. Use letters, numbers, underscores, or hyphens only.
  • Use
    bars_ago
    on
    lhs
    or
    rhs
    when you need to compare against an earlier bar. Omit it for the current bar.
  • Use
    crosses_above
    and
    crosses_below
    only when prior-bar behavior is intended.
  • Prefer
    gte
    or
    lte
    over
    eq
    or
    ne
    for floating-point comparisons.
  • Remember that
    xor
    uses parity semantics: it is true when an odd number of child nodes are true.
  • catalog.limits.max_bars_ago
    is the maximum allowed
    bars_ago
    value.
  • The
    atr
    indicator only accepts
    period
    . Passing
    source
    to
    atr
    is a validation error.
  • Indicator
    period
    params are always required. The catalog lists no default value for them; omitting
    period
    returns a 422.
  • Add
    "negate": true
    to any rule to invert its signal (fires when the condition is NOT met). Cannot be combined with
    crosses_above
    or
    crosses_below
    — use the complementary operator instead. For compound negation, apply
    negate
    to individual rules and combine with
    any
    /
    all
    logic nodes using De Morgan's laws.

Common Pitfalls

MistakeFix
Omitting
id
on a rule
Every rule in
rules[]
must have a unique
id
; logic nodes reference it
Guessing indicator defaultsAlways provide
period
; there is no server default
Passing
source
to
atr
ATR does not accept source; use
period
only
Using
eq
/
ne
on float indicators
Use
gte
/
lte
range checks instead
crosses_above
with too little data
Cross operators need one additional prior bar beyond the indicator lookback
"negate": true
with
crosses_above
/
crosses_below
Not allowed — cross operators fire on a single bar; their negation fires on ~99% of bars. Use the complementary operator instead
af_step > af_max
on
parabolic_sar
Validation error — step must be ≤ max
short_period >= long_period
on
tsi
Validation error — short must be < long
exit_signal
and
exits
both provided
Both are valid simultaneously;
exits
handles price levels,
exit_signal
handles rule-based signals
entry_price
with
next_bar_open
entry_price
only works with
next_bar_limit
or
next_bar_stop
Logic node referencing undefined idLogic node
id
must exactly match a rule
id
in the same
rules[]
array
No signals firingCheck
diagnostics.entry.rules[]
per-rule signal counts; adjust lookback, threshold, or date range

Response Discipline

  • Prefer returning concise summaries unless the user asks for raw JSON.
  • POST /api/backtests
    and
    POST /api/backtests/mine
    both return compact stored-result summaries first; do not assume the full backtest payload is in the initial response.
  • For stored results, summarize the compact summary first and only fetch the full result,
    trades
    , or
    equity_curve
    when the user needs that detail.
  • Use
    ?limit=&offset=
    for
    trades
    or
    equity_curve
    only when the result is large enough that incremental retrieval is useful.
  • 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
    ,
    equity_curve
    , and
    diagnostics
    before assuming the engine is wrong.