npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lacspace/rules

v1.2.0

Published

Trading rules as data: a small, safe JSON rule language for "buy when …, sell when …" (price, SMA/EMA, RSI, MACD, ATR, Bollinger, Supertrend direction, N-candle high/low, average volume, % change; above / below / crosses above / crosses below; all/any gro

Readme

@lacspace/rules

Trading rules as data, not code. A strategy is JSON made of operands, comparisons and all/any groups, so it can be built by clicking, written by an AI, saved, backtested and evaluated server-side for alerts, always with the same result. AI only translates words into the JSON. cleanRules coerces or drops anything unknown, so nothing it writes can execute.

import { cleanRules, evaluate, backtestRules, screen, describeRules } from "@lacspace/rules";

const rules = cleanRules({
  name: "EMA cross with RSI filter",
  entry: { mode: "all", conds: [
    { a: { k: "ema", n: 20 }, op: "crossAbove", b: { k: "ema", n: 50 } },
    { a: { k: "rsi", n: 14 }, op: ">", b: { k: "num", v: 50 } },
  ] },
  exit: { mode: "any", conds: [{ a: { k: "ema", n: 20 }, op: "crossBelow", b: { k: "ema", n: 50 } }] },
  stopAtr: 2,
}); // → Rules | null (null when there's no valid entry condition)

describeRules(rules!); // "Buy when EMA 20 crosses above EMA 50 and RSI 14 is above 50; sell when …; stop 2 × ATR below entry."
evaluate(rules!, bars); // { entry: boolean[], exit: boolean[], atr, need }, read on each candle's close

Operands: num(v), price(src), sma/ema(n, src?), rsi(n), macd/macdSignal/macdHist(f, s, g), atr(n), bbUpper/bbMid/bbLower(n, m), stDir(n, m) (Supertrend direction, +1/−1), highest/lowest(n) (the N candles before this one, so a crossAbove is a real breakout), volAvg(n), change(n) (%). Ops: >, <, crossAbove, crossBelow. mul scales the right-hand side (volume > 1.5 × volAvg(20)). STRATEGY_TEMPLATES holds five starting points; OPERAND_KINDS / OPS / defaultOperand power a builder UI.

Text form 1.2.0

You can also write rules as short text. The AI produces fewer tokens than with JSON (the five templates are 54% shorter as text), there's no JSON to break, and you can show users the same text.

import { parseRules, rulesToText } from "@lacspace/rules";

const { rules, errors } = parseRules(`
  name: EMA cross with RSI filter
  buy when ema(20) crosses above ema(50) and rsi(14) > 50
  sell when ema(20) crosses below ema(50)
  stop 2 atr
`); // rules → validated through cleanRules; errors → [{ line, col, message }]

rulesToText(rules!); // the canonical text. parseRules(rulesToText(r)).rules equals cleanRules(r)
  • Values: numbers, close open high low volume, sma(n[, src]), ema(n[, src]), rsi(n), macd(f, s, g), macd_signal(…), macd_hist(…), atr(n), bb_upper(n, m), bb_mid(n, m), bb_lower(n, m), supertrend_dir(n, m), highest(n), lowest(n), vol_avg(n), change(n). Common aliases are accepted (supertrend, signal, avg_volume, roc, …).
  • Comparisons: >, <, crosses above / crosses below (also is above, crossed under, …). Put a multiplier on the right-hand side: volume > 1.5 * vol_avg(20).
  • Lines: buy when … (or entry:), sell when … (or exit:), stop N atr, target N atr, name: …. Join conditions with only and or only or on a line. ; also separates lines, and # starts a comment.

Backtest

const r = backtestRules(bars, rules, { capital: 100000, costPct: 0.4 });
r.trades;  // [{ entryI, entry, exitI, exit, qty, pnl, pnlPct, reason: "signal" | "stop" | "target" | "end" }]
r.netPct; r.winRate; r.profitFactor; r.maxDdPct; r.buyHoldPct; r.exposurePct; r.equity;

How the backtest behaves:

  • Fills: a signal is read on candle i's close and filled at candle i+1's open.
  • Stops and targets: they're checked against each later candle's high and low. If both could hit in the same candle, the stop is assumed first. The fill is at the level, or at the open on a gap through it.
  • ATR levels: ATR stop and target use ATR(14) of the signal candle, measured from the fill.
  • Costs: charged per side.
  • Direction: long only by default (direction: "both" also shorts on exit signals).
  • End of data: any open position closes at the last close.

runBacktest(bars, strategyDef, params, options) accepts any signal source.

Screen

screen(
  [{ symbol: "NABIL", sector: "Banking", bars }, …],  // closed candles, oldest first
  {
    sectors: ["Banking"],
    rules: { mode: "all", conds: [
      { a: { k: "price", src: "close" }, op: ">", b: { k: "ema", n: 50 } },
      { a: { k: "rsi", n: 14 }, op: "<", b: { k: "num", v: 40 } },
    ] },
    minTurnover: 1e7,
    rankBy: { k: "rsi", n: 14 }, order: "asc", limit: 20,
  },
); // → { hits: [{ symbol, sector, close, changePct, turnover, rank }], skipped: [{ symbol, reason }] }

The screener evaluates each symbol's last candle, using the same engine as backtests and alerts. Ranking can use any operand, or "change", "turnover" or "volume".

Warm-up (1.1.0): by default an EMA needs 2n+1 candles and RSI 3n+1, so the seed has washed out first. Pass warmup: "minimal" to screen() or evaluate(rules, bars, { warmup }) when you only need each indicator to have a value (n + 2 candles; MACD slow + signal + 1). This is useful when you have little recorded history.

operandSeries(operand, bars) returns the full series for any operand.

Built by the ShareRocketPro team, where it runs their strategy builder, backtests and alerts. The tests pin their production backtester's trades on a real NABIL 15-minute snapshot.

Licence

Lacspace Free Licence v1.0: free for personal and commercial use.