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.
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.
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"))
);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;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.