Research DSL Guide

Build custom quant studies with typed outputs and built-in linting

FactorBench Research mode is for script-level analysis beyond single-factor formulas. Generate code with AI, refine it in the app editor, and run it against point-in-time data.

symbols(300)eval_series("AAPL", "ret(1)", start, end)daily_aggregate(syms, "ret(1) > 0.03", start, end, "count_if")return object("result_type","timeseries", ...)

Syntax fundamentals

Research DSL is expression-first and deterministic. Keep scripts concise, prefer vectorized helpers, and return a structured object for rich table/chart rendering in the app.

Core rules

  • Declare variables with let, then compose with expressions and helper calls.
  • Use return as the final statement for the output value.
  • Loops and conditionals are supported, but prefer vectorized helpers.
  • Use start_date/end_date/as_of in request config to control the evaluation window.

Minimal script

let start = "2025-01-01";
let end = "2025-12-31";
let syms = symbols(100);
let values = daily_aggregate(syms, "ret(1)", start, end, "mean");
return values;

Key helper families

Most performant scripts avoid per-date loops and use built-in vector helpers for series evaluation and aggregation.

Universe and calendar helpers

  • symbols(n) - Resolve the current universe and cap to n symbols.
  • dates(start, end) - Generate a list of ISO trading dates for a range.
  • month_end_flags(ds) - Boolean mask for month-end rows.
  • shift(series, n, fill) - Shift a vector forward/backward by n.

Series evaluation helpers

  • eval_series(sym, expr, start, end) - Evaluate one expression over time for one symbol.
  • eval_series_multi(sym, exprs, start, end) - Evaluate multiple expressions in one pass.
  • daily_aggregate(syms, expr, start, end, agg) - Cross-sectional daily aggregation.
  • equity_curve(ret_series, start_value) - Build a cumulative equity curve from returns.

Collection helpers

  • list(), push(list, value) - Construct dynamic arrays.
  • object(k1, v1, ...) - Build typed object outputs.
  • filter(values, mask) - Keep values where mask is truthy.
  • zip(a, b) - Pair two arrays row-by-row.

Typed output envelope

Returning a typed envelope lets the app auto-render result chips, charts, and tables.

Envelope checklist

  • Use result_type values like table, timeseries, distribution, or timeseries_table.
  • Define columns with id + type (date, number, string, boolean).
  • Provide rows as objects matching declared columns.
  • Optional charts can describe default visuals with x and y fields.

Shape

return object(
  "result_type", "timeseries",
  "schema_version", "1",
  "columns", list(
    object("id", "date", "type", "date"),
    object("id", "value", "type", "number")
  ),
  "rows", rows,
  "charts", list(
    object("type", "line", "x", "date", "y", "value", "title", "Series")
  ),
  "meta", object("note", "optional")
);

Research script examples

Start from these templates, then refine with AI and live lint feedback in the app.

Daily breadth count
let start = "2025-01-01";
let end = "2025-12-31";
let syms = symbols(500);
let counts = daily_aggregate(syms, "ret(1) > 0.03", start, end, "count_if");
let ds = dates(start, end);
return object(
  "result_type", "timeseries",
  "schema_version", "1",
  "columns", list(
    object("id", "date", "type", "date"),
    object("id", "count", "type", "number")
  ),
  "rows", zip(ds, counts),
  "charts", list(object("type", "line", "x", "date", "y", "count", "title", "Daily Breadth"))
);
Event-study template
let start = "2022-01-01";
let end = "2025-12-31";
let syms = symbols(300);
let out = list();
for s in syms {
  let r1 = eval_series(s, "ret(1)", start, end);
  let cond = eval_series(s, "if(ret(1) > 0.05, 1, 0)", start, end);
  let next_r1 = shift(r1, -1, null);
  let hits = filter(next_r1, cond);
  out = push(out, object("symbol", s, "count", len(hits), "mean_next_day", mean(hits)));
}
return out;
Month-end equity table
let start = "2024-01-01";
let end = "2025-12-31";
let syms = symbols(400);
let daily_ret = daily_aggregate(syms, "ret(1)", start, end, "mean");
let ds = dates(start, end);
let eq = equity_curve(daily_ret, 100000);
let me = month_end_flags(ds);
return object(
  "result_type", "timeseries_table",
  "schema_version", "1",
  "columns", list(
    object("id", "date", "type", "date"),
    object("id", "portfolio_value", "type", "number")
  ),
  "rows", zip(filter(ds, me), filter(eq, me))
);

Lint-driven workflow

The validator flags inefficient patterns and suggests vectorized templates. Iterate until lint passes, then run and inspect the rendered result + code snapshot in Research mode.

Use AI + editor together

Ask AI for first-pass scripts, then validate and run inside the Research tab for rapid iterations.

Launch the app