The Rearview strategy language
A strategy is one plain-text file ending in .rvw. It says which securities to look at, when to enter and exit, how to rank candidates when there are more than free slots, and how big each position is. Rearview evaluates the script on every trading day, using only what was known at that day's close, and turns it into orders for the next session.
This page is the whole language. It's written for traders and for the AI coding agents some of them use; the last section explains how an agent can check its own work.
A first script
language 1
strategy "RSI(2) pullback with market filter" {
universe members("SP500") // index members on each date
regime close("SPY") > sma(close("SPY"), 200) // the market is in an uptrend
rsi2 rsi(close, 2)
enter long when regime and close > sma(close, 200) and rsi2 < rsi_entry
rank by -rsi2 // most oversold first
size 10% of equity, max 10 positions
exit when rsi2 > 70 or bars_held >= 10
fill next_open
param rsi_entry = 10 in 5..20 step 5
}language 1comes first in every file. Version 1 scripts keep working in every later release.- One
strategyblock holds the statements, one per line, in any order. regimeandrsi2are bindings: names for sub-expressions, used like built-in series. There is no=.rsi_entryis a param: a number with a range, which searches and walk-forward tests can vary.//starts a comment.
How a script runs
On each trading day t, at the close:
- Universe. It is worked out for day
t: index membership, tickers and kinds as they stood that day. - Exits. Every open position whose exit condition holds gets an exit order, and so does one that has been held
max barsbars. Then the positions whosescale out ... whencondition holds sell part of their shares. Stops, trailing stops and targets are watched during the next session. - Entries. Every security in the universe whose entry condition and
filterhold is a candidate. Candidates are sorted byrank by, highest first (ties go to the lower security id). They fill the free slots (max positionsminus open positions), each sized bysize. Before them, positions whoseadd whencondition holds get an add. Arebalancescript works differently, as Rebalancing describes. - Fills. The orders fill in the next session, at the open by default (
fill). Sells go first, then buys in rank order. If there isn't enough cash after a gap, the share count shrinks rather than the trade being skipped.
A value that can't be known yet, during an indicator's warm-up for instance, is unknown, and an unknown condition is never true. Nothing on day t can see day t + 1.
Statements
| Statement | What it does |
|---|---|
universe <u> | Required. The securities considered. See Universes. |
filter <condition> | A candidate must also meet this. Same as adding and <condition> to every enter. It never blocks an exit. |
<name> <expression> | A binding. Names are unique and shared with params. A binding may be used before the line that defines it. |
enter long when <condition> | At least one enter is required. The long entry signal. |
enter short when <condition> | The short entry signal. A script with both trades both sides. |
exit when <condition> | Closes a position of either side. exit long when and exit short when apply to one side. |
stop 8% | A protective stop 8% from the entry price. stop 3 x atr(20): three times the 20-day ATR at entry. |
trail 10% or trail 2 x atr(14) | A trailing stop, from the highest high (or, for shorts, lowest low) since entry. |
target 25% | A profit target 25% from the entry price. |
max bars 10 | Close a position after 10 bars. exit when ... or bars_held >= 10 means the same. |
rank by <expression> | Orders candidates when more qualify than there are free slots. Higher is preferred, so rank by -rsi2 prefers the lowest RSI. |
size 10% of equity | Each position is 10% of equity at the signal day's close. |
size equal weight | Equity divided equally across max positions. |
size $5000 per position | A fixed amount per position. |
size risk 1% with stop | Risk 1% of equity per position, with the distance taken from the ATR stop, which is then required. |
size weight <expression> | The fraction of equity given by the expression, per security and day (0.05 is 5%). |
max positions 10 | At most 10 open positions. It may also end the size line: size 10% of equity, max 10 positions. The default is 10, with a warning. |
max position 25% | No position may be more than 25% of equity. Default 100%. |
same_bar target_first | When a stop and the target both lie inside one day's range, the target fills. The default, same_bar stop_first, assumes the stop was hit first. |
flat at close | Hold nothing overnight: every open position exits at the close of its session's last bar. See Intraday bars. |
rebalance monthly | Rebalance on the first trading day of each month: hold the best-ranked candidates, each resized to its size. Also weekly, quarterly and every 21 days; , band 20% skips small resizes, and , keep top 15 keeps a holding while it ranks 15th or better. See Rebalancing. |
add when <condition> | Add one more size to a held position whose condition holds. See Scaling in and out. |
max adds 2 | At most 2 adds per trade. Default 1. |
scale out 50% when <condition> | Sell half of a position, once per trade, when the condition holds. |
scale out 50% at 10% | Sell half of a position, once per trade, at a partial profit target 10% from the entry price. |
scale out 33% at 10%, 33% at 20% | A ladder: sell a third at 10% from the entry price and another third at 20%, each a share of the position held at the first step. 2 to 8 steps, their levels rising. |
max scale outs 3 | With scale out ... when: repeat the step up to 3 times a trade. A number from 1 to 8, not a param. |
fill next_open | Orders fill at the next session's open. Default. |
fill next_close | Orders fill at the next session's close. |
fill same_close | Orders fill at the signal day's own close. It isn't tradable, so results are marked approximate and these orders never appear in a next-day order list. |
entry market | Market orders. Default. |
entry limit <price> | A limit order at a price worked out on the signal day, such as entry limit close * 0.98. It only works with fill next_open and lasts one day. |
entry stop <price> | A stop order to enter on strength, such as entry stop high + $0.05. It only works with fill next_open and lasts one day. |
param <name> = <default> in <low>..<high> step <step> | A tunable number. See Params. |
Every statement except bindings and params may appear once. enter long, enter short, exit, exit long and exit short each count as their own statement.
A strategy with no way to exit (no exit condition, stop, trail, target or max bars) only closes positions when the data ends, and gets a warning.
bars_held
bars_held is allowed only as a top-level or term of an exit, in the form bars_held >= 10, where the number is a whole number or a param. The checker turns it into max bars 10. Anywhere else it is an error. In a strategy that trades both sides, it can't be in exit long or exit short either, because max bars holds for both sides; put it in exit when.
Values and units
Every expression has a unit, and the checker refuses combinations that make no sense, such as comparing a price with an RSI.
| Unit | Meaning | Examples |
|---|---|---|
| Price | dollars per share | close, sma(close, 20), atr(14), close - open |
| Volume | shares | volume, sma(volume, 20) |
| Dollars | an amount of money | dollar_volume, adv(20), $5000, close * volume |
| Percent | a 0 to 100 scale | rsi(close, 2), stoch_k(14), rank_pct(x) |
| Ratio | a fraction | close / open, roc(close, 10), ibs(), 10% |
| Count | a whole number of things | bucket(x, 10), count(close > open, 5) |
| Bool | true or false | any comparison, crosses_above(a, b) |
| Number | a plain number | 2, 0.5, a param; it takes on the other side's unit |
- Percent literals are fractions.
10%is 0.10, a Ratio. The RSI family runs from 0 to 100 instead, so writersi(close, 2) < 10;rsi(close, 2) < 10%is an error.x * 100turns a Ratio into a Percent andx / 100turns a Percent into a Ratio. - Money literals such as
$5read as a price next to a price:close > $5. - Comparisons (
< <= > >= == !=) need the same unit on both sides, or a plain Number on one side. Arithmetic:
+and-keep the unit;Price / Priceis a Ratio;Price * Volumeis Dollars;- multiplying or dividing by a Number, Ratio or Count keeps the unit.
- Conditions of
filter,enterandexitmust be Bool, andrank bymust be a number. Combine conditions withand,orandnot.
x[k] is the value k bars ago: close[1] is yesterday's close, and (close > open)[1] is whether yesterday was an up day. k is a whole number from 0 to 10,000, written out; to go back a param's number of bars, use lag(x, n).
Syntax
language 1 comes first, then exactly one strategy "name" { ... } or scan "name" { ... } block. Blank lines and // comments can go anywhere, before language 1 too.
Lines
A statement ends at the end of its line, or at the } that closes the block. It carries on over more lines only inside ( ) or [ ]. Anywhere else, a line that ends with an operator (and, or, +, > and the rest) or a comma is an error, so put a long condition in parentheses to wrap it:
language 1
strategy "A long entry condition" {
universe members("SP500")
enter long when (close > sma(close, 200)
and rsi(close, 2) < 10
and volume > sma(volume, 50))
size 10% of equity, max 10 positions
max bars 10
}Names
Binding and param names are made of ASCII letters, digits and _, and don't start with a digit. Case matters: Close isn't close. These words belong to the language, and no binding or param can take one:
language strategy universe filter enter exit stop target trail max rank
size fill entry param and or not except when by of in step long short
per with all stocks etfs crypto futures forex members symbols sector
industry same_bar flat rebalance add scale
open high low close volume dollar_volume vwap time bar_of_day bars_heldWords that mean something at only one point of one statement, such as equity, weight, market, next_open or exchange, are free to use as names. So are function names, because a call always has parentheses: sma rsi(close, 2) makes a binding named sma, and sma(close, 20) is still the average.
Numbers, times and strings
- Numbers are written
20or0.5. There is no exponent (1e5), no leading point (write0.5, not.5) and no separator (write1000, not1,000or1_000). A-in front is the minus operator. - Whole numbers have no point. Where one is needed (a window, a count,
x[k],bars_held >= N,language 1), write10, not10.0. - Percents and money:
10%and$5000, with the%or$touching the number (see Values and units). A param name with%straight after it, such asstop_pct%, reads the param as a percentage, and only where a param sets a statement, as instop stop_pct%(see Params).%is never a remainder. - Times are written
HH:MM, from00:00to23:59:09:30, not9:30. See Intraday bars. - Strings are written in straight double quotes on one line, as in
"SPY". Single quotes, and the curly quotes some editors type, are errors. A string can hold any character but", and there are no escapes.
Operators
From the tightest to the loosest:
x[k], the valuekbars ago-x*and/+and-- the comparisons
<,<=,>,>=,==and!= notandor
So:
close - open * 2isclose - (open * 2).a or b and cisa or (b and c).- Operators on the same level work from left to right:
a - b - cis(a - b) - c. - Comparisons don't chain: write
a < b and b < c, nota < b < c. notis looser than a comparison but tighter thanand:not close > openmeansnot (close > open), andnot a and bmeans(not a) and b.notcan't follow a comparison or arithmetic operator without parentheses:1 < not cis an error.
A universe has operators of its own; see Universes.
Prices, splits and dividends
Rearview stores the prices you actually traded at and computes adjusted series itself, so a split or dividend never looks like a price move. Each function runs on the series that suits it:
- As traded: the bare series and arithmetic on them.
closeis today's actual close andclose > $5compares it with five dollars.ibs(),adv(n)anddollar_volumeare as traded too. - Split-adjusted: averages, ranges and bands (
sma,ema,highest,atr,bollinger_*,keltner_*,stoch_k,adxand the rest of the price-based functions), plusx[k],lag,changeand the cross functions. A value read on daytis in the shares of dayt, soclose > sma(close, 200)compares like with like across a split. - Total return: percentage indicators (
rsi,roc,volatility,percent_rank,rsi_composite,efficiency_ratio), so neither splits nor dividends look like returns.
The adjustments only ever use the past: an indicator on day t never knows about a split after t.
When a run starts
A run starts on the first day a stock or ETF in your data has a bar, or on the start date you choose if that is later. A strategy of coins only starts on the first day a coin has a bar, and one that holds both on the earlier of the two. A strategy that can hold futures counts their bars as it counts a stock's. Days on which only an index has bars are left out: nothing could trade on them, and counting them would add years of cash to every result. Data whose market index goes back to 1927, but whose first stocks start in 1962, gives runs from 1962. A start date before that day moves to it.
The first day comes from the data alone, so every script on the same data starts on the same day and its runs compare like-for-like. A script can still read something that starts later: another ticker, such as the ETF in close("SPY") > sma(close("SPY"), 200), or the run's benchmark. Until that ticker has bars, whatever reads it is unknown, so a filter on it holds the run in cash. The run then warns SYMBOL_STARTS_LATE (or BENCHMARK_UNAVAILABLE for the benchmark) and names the day the ticker starts. In the app, Start on in the warning moves the run's start to that day. On the command line, set start in the config's run.
Indicators still need their lookback after the start. When a run starts on a stock's first bar, sma(close, 200) is unknown for its first 199 bars. When you choose a later start, the bars before it count toward the lookback.
Missing values
A value that can't be computed is unknown: an indicator's warm-up, a missing bar of another ticker, a division by zero, a cross-section with nothing to rank. Unknown values spread through arithmetic, and a comparison with one is unknown. Conditions use three-valued logic:
false and unknownis false;true or unknownis true;not unknownis unknown;- an unknown condition never holds, so
not (close > sma(close, 200))does not hold during the warm-up either.
Other tickers
close("SPY"), open("SPY"), high(...), low(...) and volume(...) read another ticker's series. The ticker is looked up on each date, because tickers change hands. An expression built only from one ticker, such as sma(close("SPY"), 200), is computed on that security's own bars, with its own splits. On a day the ticker has no bar, its values are unknown; a stock read on a weekend by a run that trades every day is the exception (Coins and the exchange's days). A ticker the data doesn't have at all gets a SYMBOL_NOT_IN_DATA warning: whatever reads it is unknown every day, so check the spelling or add its prices.
Intraday bars
On intraday data (see Intraday bars in the import guide), a bar is one moment of a session, such as the 5 minutes from 10:05. A script runs at every bar, and every lookback counts bars, so sma(close, 200) is 200 bars, not 200 days. To work in days, use daily.
timeis the bar's start, and10:00a time writtenHH:MM. A time compares only with a time:time >= 10:00 and time < 15:30.bar_of_dayis the bar's place in its session, 1 for the first.daily(x)evaluatesxon daily bars: the completed days plus today so far.daily(high)is today's high so far,daily(close[1])is yesterday's close, anddaily(sma(close, 200))averages 199 completed days' closes and today's close so far.vwapis the session's average price weighted by volume so far, from each bar's(high + low + close) / 3. While the session has traded nothing, it is the bar's own typical price.flat at closeholds nothing overnight: every open position exits at the close of its session's last bar, half days included. It can't go withrebalance.
On daily data, daily(x) is simply x, vwap is the day's typical price, and each day is a session, so flat at close sells at the close what the open bought. time and bar_of_day are an error there (INTRADAY_ONLY).
daily(vwap) isn't the session's vwap: inside daily, a whole day is one bar, so vwap there is that bar's typical price. The same goes for weekly and monthly.
Weekly and monthly bars
weekly(x) and monthly(x) evaluate x on weekly or monthly bars. On each day, those are the completed weeks (months) plus the one in progress, built from the days so far, so nothing after today is used.
weekly(close)is today's close;weekly(high)is the week's high so far;weekly(sma(close, 10))averages nine completed week closes and today's close;sma(weekly(close), 10)runs on daily bars again: it is the 10-day average ofweekly(close).
Universes
The universe is worked out on each date. It limits entries; exits always apply.
| Universe | Securities |
|---|---|
all | every stock and ETF |
stocks, etfs | by kind |
crypto | every coin |
futures | every futures chain |
forex | every forex pair |
exchange("ASX") | every security of that exchange but coins; exchange("US") holds the US exchanges' and OTHER's |
members("SP500") | the members of an index label from your data, on that date |
symbols("SPY", "QQQ") | the securities holding those tickers on that date |
sector("Energy"), industry("Software") | the securities with exactly that label (case matters) |
a and b, a or b, a except b | both, either, the first without the second |
and and except bind tighter than or; use parentheses to group. An index (kind INDEX) is never traded, and a security is only in the universe on days it has a bar. all, stocks and etfs hold every exchange's stocks and ETFs. all holds no coins, futures or forex pairs, so a strategy trades them only when its universe names them, with crypto, futures, forex or otherwise; see Coins and the exchange's days, Futures and their rolls, Several exchanges' days and Currencies and forex pairs. Sector and industry are each security's one current label, not their history, and a run that uses them says so in its assumptions.
Rebalancing
A strategy with rebalance holds a target portfolio and trades back to it on a schedule:
language 1
strategy "Monthly momentum, rebalanced" {
universe members("SP500")
mom roc(close, 126)
enter long when mom > 0
rank by mom
size equal weight, max 10 positions
rebalance monthly, band 20%
}- When.
weekly,monthlyandquarterlyrebalance on the first trading day of each week, month or quarter: the day after a week, month or quarter has ended. The run's first day (When a run starts) is a rebalance day too, so the strategy invests at once.every 21 dayscounts trading days from the first day of the run. A strategy of coins only counts every day instead; one that holds coins and anything else counts the exchange's days. Orders fill at the next session, asfillsays. - What it holds. On a rebalance day the candidates are the securities whose
enter(andfilter) holds, the ones already held included. They are taken byrank by, best first, up tomax positions. Each is sized bysize, exactly as an entry would be. A held position that is no longer among them is sold. One that is gets trimmed or topped up to its size. New ones are bought. A security that would get 0 shares, such as one with a weight of 0, doesn't take a place, and the next candidate does. - Keeping holdings.
rebalance monthly, keep top 15lets a held security keep its place while it ranks 15th or better among the candidates. Withmax positions 8, a holding that slips from 8th to 12th isn't sold; one that slips below 15th, or stops qualifying, is. New names fill only the places the kept holdings leave, best first, and when more holdings rank in the top 15 than there are places, the best-ranked keep theirs. This trades less at the edge, where names hover around the cut.keep topmust be at leastmax positions. A holding with no bar that day, or sized to 0 shares, isn't kept. Withoutkeep top, a holding is sold as soon as it drops out of the topmax positions. - The band.
band 20%leaves a held position alone when its resize would be smaller than 20% of its target size. Without a band every target is traded back to its exact size. - Between rebalance days the strategy makes no new entries. Exits, stops, targets and time exits still work every day, and a position they close leaves its place empty until the next rebalance.
- Missing data. A held position whose security has no bar on a rebalance day keeps its shares and its place. One whose size can't be worked out that day (an unknown weight, say) keeps its shares too.
rebalancecan't be combined withadd when,scale outor limit and stop entries: it resizes positions itself.
Scaling in and out
add when adds to a winner (or to any held position whose condition holds) on a day its security is in the universe. Each add is one more size, cut so the position stays within max position. max adds caps the adds per trade (one by default). Adds are market orders, even when entries are limit or stop orders.
language 1
strategy "Breakout with pyramiding" {
universe members("SP500")
enter long when close > highest(close, 50)[1]
add when close > highest(close, 20)[1]
max adds 2
size 5% of equity, max 8 positions
max position 15%
scale out 50% at 15%
trail 3 x atr(20)
}scale out sells part of a position, once per trade:
scale out 50% when <condition>: at the next fill after the condition holds, like an exit;scale out 50% at 15%: at a partial profit target 15% from the entry price, watched during the session liketarget. It must be closer thantarget. When a stop and the partial target are both reached on one day,same_bardecides which came first.
A ladder sells in steps. scale out 33% at 10%, 33% at 20% sells a third 10% above the entry price, another third at 20%, and leaves the rest to the strategy's exits. Each step's share is of the position held when the first step sold, so the thirds stay thirds; the last step sells at most what's left. The levels rise and stay closer than target. One day can fill several steps in order, each at its level, or at the open when the day opens beyond it. With a condition, scale out 25% when rsi2 > 70 and max scale outs 3 sell a quarter on each of up to three days the condition holds, one step a day. A ladder has 2 to 8 steps.
After an add, the entry price that stops and targets are measured from is the average cost of the shares held, so they move with it. Trimming or scaling out leaves it alone. A trade's entry price in the trade list is the average of all its buys.
Cross-sectional functions
rank_pct(x), cs_zscore(x) and bucket(x, k) compare securities with each other on each date. They are computed among the securities in the universe that day whose x is known; the others get no value. The result is a series like any other, so sma(rank_pct(roc(close, 20)), 5) works.
Your own data: series("name")
series("pe_ratio") reads series/pe_ratio.csv from the data you imported: the current security's latest value dated on or before the day, or unknown before its first row. Use it for scores, fundamentals or anything else you have per security and date.
A value dated d is used from day d's close on. Date each row by when the value became known (a filing date, not the end of the quarter it describes), or the backtest will use it too early. A run fails, naming the series the data does have, when the name doesn't exist.
Params
param n = 20 in 10..50 step 10declares n with default 20, which searches can set to 10, 20, 30, 40 or 50. The low end must be below the high end, the step must divide the range, and the default must lie within it. Values may be negative or fractional.
A param is a plain Number in expressions. It can also stand for a window (sma(close, n), atr(n)), as long as every value in its range is a whole number the function accepts.
A param can also set a statement, so a search can try different stops or position counts:
language 1
strategy "Breakout with a tunable stop" {
universe members("SP500")
enter long when close > highest(close, 50)[1]
size equal weight, max n positions
stop stop_pct%
max bars hold
param stop_pct = 8 in 4..12 step 2
param n = 10 in 5..20 step 5
param hold = 20 in 10..40 step 10
}- Percentages (
stop,trail,target,size ... of equity,size risk,max position,scale out, the band ofrebalance): write the param with%. It holds the number of percent, sostop_pct = 8is an 8% stop. - ATR multiples:
stop k x atr(20). - Counts:
max positions n,max bars hold,bars_held >= holdin an exit,max adds n,rebalance every n days,keep top n.
Every value in the param's range must make sense there: a percentage above 0 and within its limit, a multiple above 0, a count that is a whole number from 1 to 1,000,000.
A walk-forward can vary these too. Its joined out-of-sample run trades each fold's winning setting from the fold's first day, and the positions it already holds switch to that setting the same day: a new stop is measured from their average cost, a new ATR multiple scales the distance they had, and a lower max positions sells nothing but opens nothing new until fewer are held. The run's assumptions list what changed on which day.
Scans
A scan lists the securities that meet some conditions on a day, with their values as columns, as a stock screen does. It is a script whose header says scan instead of strategy:
language 1
scan "Oversold above the 200-day" {
universe members("SYN500")
rsi2 rsi(close, 2)
trend close > sma(close, 200)
filter trend and rsi2 < rsi_max
rank by -rsi2
param rsi_max = 10 in 5..20 step 5
}- What a scan holds:
universe(required), bindings,filter,rank byandparam. A statement that trades, such asenter,exit,size,stoporrebalance, is an error (SCAN_STATEMENT).scanmeans something only at the top of a file, so a binding may still be namedscan. - What it lists on day
t: exactly the entry candidates a strategy with the same universe, bindings, filter and rank would have at that day's close: in the universe with a bar that day,filtertrue, the rank known, and never an index. It uses only what was known at the close, as a strategy does. - The order: best ranked first, ties going to the lower security id; by ticker without
rank by. - A strategy can be scanned too. It then lists its entry candidates on each side it trades, with a
sidecolumn: the securities it would have ranked for free slots that day. These are exactly its signals' entry rows. - The columns: the day, the security (
id,ticker,name,kind, andcurrencywhen the data has several),sidefor a strategy,positionandscore, the day's bar as traded, every binding with a value on the security's own bars in that day's units, and each condition as true, false, or empty while unknown. A scan's binding can't be named like one of its own columns (date,id,ticker,name,kind,currency,side,position,score,passesor a bar's): that'sSCAN_COLUMN_NAME. A strategy's binding that is, or that is named like a condition (enter_long, say), is listed with_bindingafter its name, such asscore_binding(again, while another binding has that name). - The days: the data's last day by default, behind a sealed lockbox the last day before it; or one day, or every day of a range, a block of rows each. On intraday data a day is a moment, and a range lists every moment. More than 1,000,000 rows is refused (
SCAN_TOO_LARGE), with the count.
A scan can't be run, optimized, walked forward, traded live or added to a portfolio (SCAN_NOT_RUNNABLE). It shows no returns, so it isn't counted as a try. Once the trial has ended without a licence it stops, as a run does.
In the app, ⌘R on a scan scans it, and Run ▸ Scan Candidates (⇧⌘R) scans a strategy. File ▸ New Scan starts one from a template. The results area lists the rows in a table you can sort by any column; step a day back or forward, choose Range, or Every Member to add the members that don't pass, with passes false. Export CSV… saves the rows. The evening routine can run your scans after the orders (see The evening routine).
From the command line:
rvw scan --store <data store> --script <scan.rvw> --out rows.csv
rvw scan --store <data store> --script <scan.rvw> --from 2025-11-03 --to 2025-11-28 --every --out rows.csvIt writes the CSV and prints what it wrote as JSON: the script, its kind, the days, how many members and rows, and each column with its unit and where it comes from. --date gives one day, --param sets a param, and the lockbox options are those of rvw run.
Several strategies
A portfolio file (.rvwp) runs 2 to 8 scripts together, each with its params and a weight: its share of the starting cash. In the app, add one with New Portfolio from the + menu under the navigator; on the command line, rvw run --portfolio two.rvwp.
- Shared cash. One account. A security belongs to one strategy at a time, each strategy sizes from its weight's share of the equity, and when cash runs short the strategy listed first is funded first. Every strategy must use the same
fill. - Fixed sleeves. One account per strategy, each starting with its weight's share of the cash. Weights under 100% leave the rest idle, earning nothing. By default nothing moves between sleeves, so a winning sleeve's share of the whole grows.
The portfolio file
The app writes the file for you. To write one yourself, it's JSON:
{
"format": "rvw-portfolio",
"version": 1,
"mode": "sleeves",
"strategies": [
{"script": "rsi2_pullback.rvw", "weight": 500000,
"params": {"rsi_entry": 15}, "name": "Pullback"},
{"script": "breakout_atr.rvw", "weight": 400000}
]
}formatandversionare always"rvw-portfolio"and1.modeis"shared"for shared cash or"sleeves"for fixed sleeves.strategieslists the 2 to 8 scripts, each with these keys:scriptis the script's path from the portfolio file's folder;paramssets params by name, each one the script declares and within its range, and can be left out to keep every param at its default;weightis the strategy's share in whole millionths, so500000is 50%. Each is from 1 to 1,000,000, and together they add up to at most 1,000,000;namelabels the strategy in the results, and can be left out to use the script's file name without.rvw.
- The order of
strategiescounts: in shared cash the first is funded first, sleeves pass cash in this order, and in a search the first one's weight is0.weight. rebalance, for sleeves only, is in the next section.- Any other key is an error.
Rebalancing between sleeves
Sleeves can move cash back towards their weights on a schedule. In the app it's Rebalance between sleeves in the portfolio editor; in the file:
"rebalance": {"every": "quarter", "band": 50000, "sell": true}every is "week", "month", "quarter" or {"days": 21}, as for a script's rebalance, band is in millionths (50000 is 5%; 0 by default), and sell is Sell to rebalance (off by default).
- On each rebalance day except the run's first, after the day's trading, each sleeve's target is its weight's share of the whole: the sleeves' equity plus the idle cash.
- Sleeves above their target pass cash to those below, in portfolio order, with the idle cash last. A sleeve gives no more than its excess, the cash it holds, or its equity above its margin requirement. Without selling, a fully invested sleeve has nothing to give.
- Sell to rebalance: a sleeve that couldn't give enough sells part of each of its longs, all in the same proportion, to raise the rest. The sales are market orders at the next session (Sold to rebalance in the trade list), and the cash moves the day after. A long without a bar that day, or with an exit order, isn't sold, and shorts never are. At that close the sleeve makes no other orders, so its own buys don't spend the cash; a strategy that rebalances itself waits for its next rebalance day.
- A sleeve within the band of its target takes no part, and neither does a wiped-out one.
- Each move is a
TRANSFERevent of the sleeve on that day, and orders made at that close size from the new equity. By strategy shows each sleeve's net cash moved. Its CAGR and the correlations leave the moves out, so a sleeve that was given cash doesn't look as if it had made it.
Searching weights
A portfolio search can vary weights as well as params. 0.weight is the first strategy's weight in percent, 1.weight the second's:
rvw search --portfolio two.rvwp --store data --out search \
--space 0.weight=20..60:10 --space 1.weight=20..60:10- A combination whose weights add up to more than 100%, or that includes a 0, is left out before the search runs (a genetic or CMA-ES search skips such a setting when it comes up).
search.jsonsays how many asleft_out, and only the trials that ran count for the Deflated Sharpe and PBO. - Varying a weight doesn't evaluate the scripts again; only the engine runs.
- In the app, each strategy has a Weight, % row in Optimize, off until you turn it on. Use These Values writes the best weights into the file.
- A walk-forward can vary them too. Each window picks its weights with its params, and the joined run starts with the first window's weights and changes them on each later window's first day where they differ; its assumptions list the dates. Sleeves move towards new weights by rebalancing, so turn on Sell to rebalance, or a fully invested sleeve stays where it drifted.
- In a portfolio search a script can't have a param named
weight, since0.weightnames the weight.
Coins and the exchange's days
Coins (kind CRYPTO) trade every day; stocks, ETFs and indexes trade on the exchange's days. The crypto universe holds the coins, and a strategy can hold them alone or beside anything else:
language 1
strategy "Coin trend with a market filter" {
universe crypto
regime close("SPY") > sma(close("SPY"), 200)
enter long when regime and close > sma(close, days)
exit when close < sma(close, days) or not regime
rank by roc(close, days)
size 30% of equity, max 3 positions
param days = 50 in 20..100 step 10
}- A run's days. A run trades every day when one of its strategies can hold a coin, and on the exchange's days otherwise. What a strategy can hold comes from its universe over the whole data:
all,stocksandetfshold no coins,cryptoholds only coins, andmembers,symbols,sectorandindustryhold a coin when one matches them. A stock script on data with coins gives the same results as on the same data without them. - Each security keeps its own days. A stock's order made on a Friday fills on Monday, and a coin's on Saturday. A weekend isn't a halt: the order waits for its security's next session rather than being cancelled. Indicators run on each security's own bars, so
sma(close, 50)covers 50 days for a coin and 50 trading days for a stock, andbars_heldandmax barscount bars. - A stock on a weekend. On a day that isn't a stock's session,
close("SPY"), anything built only from it, and its series values are those of its last session, Friday's. On a session without a bar (a halt) they are unknown, as always. - Cross-sections rank the securities with a bar that day, so on a weekend
rank_pctandbucketrank the coins among themselves. - Schedules.
rebalancecounts the exchange's days in a strategy that can hold anything besides coins, so a monthly rebalance of SPY, TLT and BTC falls on the month's first exchange day, and every day in a strategy of coins only. Rebalancing between sleeves counts the exchange's days unless every strategy holds only coins, and a sleeve's trims fill at each security's next session. - Long only, with cash. A short entry or short rebalance target on a coin is dropped, with the warning
NO_COIN_SHORTS. In a Reg T account a coin needs the cash for it and its commission, and its margin requirement is 100% of its value. - A year is 365 days in a run that trades every day: Sharpe, Sortino and volatility are annualised with √365, and rolling one-year windows are 365 days long.
- Quantities of a coin come in steps of 0.00000001 unless Coin step in Run settings says otherwise.
The examples' coins folder has this script, and a portfolio of an ETF sleeve beside a coin sleeve.
Futures and their rolls
A futures chain (kind FUTURE; see Futures in the import guide) is the contract you'd hold each day, rolled from one to the next. The futures universe holds the chains, and a strategy can hold them alone or beside stocks:
language 1
strategy "Futures trend, long or flat" {
universe futures
enter long when close > sma(close, 100) and roc(close, 50) > 0
rank by roc(close, 50)
stop 3 x atr(20)
size risk 1% with stop, max 3 positions
exit when close < sma(close, 50)
}- What a strategy can hold.
futuresholds only chains;all,stocks,etfsandcryptohold none;symbols,members,sectorandindustryhold a chain that matches them.symbols("SPY") or futuresholds both. A stock script on data with futures gives the same results as on the same data without them. - Prices across a roll.
closeis the price of the contract held that day, soclose > 4000compares it. Indicators,x[k]andchangeread the chain adjusted by ratio at each roll, as they read a stock adjusted for splits, sosma(close, 100)never mistakes a roll's gap for a move. - Contracts and their value. A chain trades in whole contracts, and every amount from a quantity counts the point value: one ES contract at 5,000 is worth $250,000. Sizing by
% of equityor byriskto a stop,max positionand the exposure limits use that value, so an account too small for one contract sizes to none. - Rolls. A held chain rolls on its own on the roll's date, at the previous close: out of the old contract and into the new one, each leg paying slippage and commission. The trade carries on, flagged *Rolled*, and its stop, target and pending limit and stop prices move by the roll's ratio.
- Daily settlement. Each position settles at every close, so its gains and losses reach cash that day. A cash account backs each contract with cash for its full value: it never borrows, and a short entry is refused. A Reg T account holds futures on margin, long or short; see Account in Run settings.
- Days. Chains trade on the exchange's days, and a year has 252 of them.
The examples' futures folder has this script, and a portfolio of an ETF sleeve beside a futures sleeve.
Several exchanges' days
Each security trades on its exchange's days (see Exchanges and their days in the import guide): a US stock on the US exchanges', an ASX stock on the ASX's. One strategy can hold both:
language 1
strategy "Momentum across exchanges" {
universe stocks
mom roc(close, 126)
enter long when mom > 0 and close > sma(close, 200)
rank by mom
size equal weight, max 10 positions
rebalance monthly, band 20%
}- What a strategy can hold.
all,stocksandetfshold every exchange's;exchange("ASX"),symbols,members,sectorandindustryhold the securities that match them, and a signals folder the ones its rows name. The run'sconfig.jsonlists the exchanges asexchangeswhen they aren't just the US's. - A run's days are the days any of its strategies' exchanges trades, and every day when one can hold a coin. Another exchange's securities don't change a run that can't hold them.
- Each security keeps its own days, as coins do: an order waits for its security's next session, over the other exchange's holidays too, and a closed day isn't a halt. On a day its exchange is closed,
close("BHP")and anything built only from it are its last session's. - Schedules.
rebalancecounts the days on which every exchange of the strategy trades, so a monthly rebalance of US and ASX stocks falls on the month's first day both are open. Sleeves rebalance on the days any of their strategies would. - The first day is the first day a stock or ETF of the strategy's exchanges has a bar.
- A year is 252 days on one exchange's days. On several, it is the run's days per 365.25 calendar days,
half_even(365.25 × (N − 1) / S)for N days over S calendar days, or 252 for a run of under a year. The run's Assumptions say which. - Times of day aren't modelled: each date's bars count as known at that date's end on every exchange, so a fill at the signal's own close stays approximate.
Currencies and forex pairs
Your account has one currency, US dollars unless Run Settings ▸ Account ▸ Currency (run.currency) says otherwise. Each security's prices are in its own currency (see Currencies in the import guide).
- What is in which currency. Prices, and every level built from them (
close > $5, stops, limits, targets, the entry and exit prices of a trade), are in the security's currency. Money is in the account's: cash, equity, P&L, commissions (a per-share commission too), interest,size $10,000and the margin minimums. A percentage commission is a share of the converted value. - Converting. An amount from a price converts at the day's rate from the data's forex pairs: at the pair's open for fills at the open and during the session, at its close for marks, fills at the close and sizing. A security's P&L therefore includes its currency's moves against yours.
- No rate yet. A security whose currency has no rate yet (no pair links it, or the pair's first bar hasn't come) isn't bought, with the warning
NO_RATE. A script can still read its prices. - A trade's return in the trade list is its P&L over its entry's value only when both are in the same currency; one priced in another currency shows none.
Forex pairs (kind FOREX) trade too. The forex universe holds them:
language 1
strategy "Forex trend, long or flat" {
universe forex
enter long when close > sma(close, 100) and roc(close, 50) > 0
rank by roc(close, 50)
stop 3 x atr(20)
size risk 1% with stop, max 3 positions
exit when close < sma(close, 50)
}- Units. A pair trades in whole units of its base currency: 100,000 of
EURUSDis €100,000, worth about $110,000 at 1.10. - Daily settlement, as for futures: each position settles at every close in the pair's quote currency, converted. A cash account backs each pair with cash for its full value and refuses a short entry; a Reg T account holds pairs on margin, long or short, 5% of their value to open and 4% to keep unless Advanced ▸ Forex margin says otherwise.
- Carry. Each day a pair is held it earns its base currency's interest rate and pays its quote's on its value, counted act/360, from the data's
rates.csv; a short does the opposite. A currency without a rate counts 0, and the trade is flagged *Assumed rate*. Carry is part of the trade's P&L and shows among the run's costs. - No volume cap and no haircut. A pair's volume isn't used, and a pair that ends closes at its last close.
- Live accounts can't trade pairs, nor a security in another currency than the account's.
The examples' world folder has these scripts, momentum on one exchange for an account in any currency, and a portfolio of an ETF sleeve beside a forex sleeve.
Functions
In the signatures:
x,aandbare expressions;cis a condition;n,m,k,fast,slowandsignalare numbers written as a literal or a param, never an expression.
A window is a whole number from 1 to 10,000.
Moving averages
sma(x, n)
Simple moving average.
- Result: the unit of
x. - Formula: the mean of the last
nvalues. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close > sma(close, 200)
ema(x, n)
Exponential moving average.
- Result: the unit of
x. - Formula: seeded with the
smaof the firstnvalues, thene = e + 2 / (n + 1) * (x - e). - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
ema(close, 12) > ema(close, 26)
wma(x, n)
Linearly weighted moving average: the newest value weighs n, the oldest 1.
- Result: the unit of
x. - Formula:
(1 * x[n-1] + ... + n * x) / (n(n+1)/2). - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close > wma(close, 20)
dema(x, n)
Double exponential moving average, which lags less than ema.
- Result: the unit of
x. - Formula:
2 * ema(x, n) - ema(ema(x, n), n). - Price series: split-adjusted.
- Warm-up:
2n - 2bars. - Example:
close > dema(close, 20)
tema(x, n)
Triple exponential moving average, which lags less than dema.
- Result: the unit of
x. - Formula:
3 * ema(x, n) - 3 * ema(ema(x, n), n) + ema(ema(ema(x, n), n), n). - Price series: split-adjusted.
- Warm-up:
3n - 3bars. - Example:
close > tema(close, 20)
hma(x, n)
Hull moving average: smooth, with little lag.
- Result: the unit of
x. - Formula:
wma(2 * wma(x, n / 2) - wma(x, n), sqrt(n)), the half and the root rounded down. - Price series: split-adjusted.
- Warm-up:
n + sqrt(n) - 2bars, the root rounded down. - Example:
close > hma(close, 21)
kama(x, n)
Kaufman's adaptive moving average: close to price in a trend, nearly still when price chops.
- Result: the unit of
x. - Formula:
k = k + sc * (x - k), wheresc = (er * (2/3 - 2/31) + 2/31)^2anderis theefficiency_ratioovernbars (0 for a flat window); seeded with the value before the first one. - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
close > kama(close, 10)
median(x, n)
Median of the last n values.
- Result: the unit of
x. - Formula: the middle of the sorted values; the mean of the two middle ones when
nis even. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close > median(close, 20)
vwma(x, n)
Volume-weighted moving average.
- Result: the unit of
x. - Formula:
sum(x * volume) / sum(volume)over the lastnbars; unknown when the volume is 0. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close > vwma(close, 20)
adv(n)
Average dollar volume.
- Result: Dollars.
- Formula:
sma(close * volume, n)in dollars as traded. - Price series: as traded.
- Warm-up:
n - 1bars. - Example:
adv(20) > $1000000
Momentum and oscillators
rsi(x, n)
Relative strength index with Wilder smoothing, 0 to 100.
- Result: Percent.
- Formula: average gain and loss of the changes, seeded with plain means of the first
nand thenavg = (avg * (n - 1) + current) / n;100 - 100 / (1 + gain / loss), 50 with no change at all. - Price series: total return.
- Warm-up:
nbars. - Example:
rsi(close, 2) < 10
roc(x, n)
Rate of change over n bars, as a fraction (0.10 is +10%).
- Result: Ratio.
- Formula:
x / x[n] - 1. - Price series: total return.
- Warm-up:
nbars. - Example:
roc(close, 60) > 0
rsi_composite(x)
A three-part short-term reversion score, 0 to 100.
- Result: Percent.
- Formula: the mean of
rsi(x, 3), a 2-bar RSI of the up-or-down streak length, and the percentile of today's 1-day change among the last 100. - Price series: total return.
- Warm-up: 101 bars.
- Example:
rsi_composite(close) < 15
stoch_k(n)
Stochastic %K: where the close sits in the range of the last n bars, 0 to 100.
- Result: Percent.
- Formula:
100 * (close - lowest low) / (highest high - lowest low)overnbars; unknown for a flat range. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
stoch_k(14) < 20
stoch_d(n, m)
Stochastic %D: %K smoothed.
- Result: Percent.
- Formula:
sma(stoch_k(n), m). - Price series: split-adjusted.
- Warm-up:
n + m - 2bars. - Example:
stoch_k(14) > stoch_d(14, 3)
cci(n)
Commodity channel index of the typical price.
- Result: Number.
- Formula: with
tp = (high + low + close) / 3:(tp - sma(tp, n)) / (0.015 * mean deviation of tp). - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
cci(20) < -100
mfi(n)
Money flow index: a volume-weighted RSI, 0 to 100.
- Result: Percent.
- Formula: typical price times volume, counted as up or down flow as the typical price rises or falls;
100 - 100 / (1 + up / down)overnbars, 50 with neither. - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
mfi(14) < 20
ppo(x, fast, slow)
Percentage price oscillator: the gap between a fast and a slow average, as a percent of the slow one.
- Result: Percent.
- Formula:
100 * (ema(x, fast) - ema(x, slow)) / ema(x, slow). - Price series: split-adjusted.
- Warm-up:
slow - 1bars. - Example:
ppo(close, 12, 26) > 0
trix(x, n)
TRIX: the one-bar percent change of a triple-smoothed average.
- Result: Percent.
- Formula:
100 * (t / t[1] - 1)wheret = ema(ema(ema(x, n), n), n). - Price series: total return.
- Warm-up:
3n - 2bars. - Example:
trix(close, 15) > 0
cmo(x, n)
Chande momentum oscillator, -100 to 100.
- Result: Percent.
- Formula:
100 * (up - down) / (up + down)over the lastnchanges, whereupanddownadd up the rises and the falls; 0 when nothing changed. - Price series: total return.
- Warm-up:
nbars. - Example:
cmo(close, 14) > 50
stoch_rsi(x, n)
Stochastic RSI, 0 to 100: where the rsi sits in its own range of the last n bars.
- Result: Percent.
- Formula:
100 * (r - lowest(r, n)) / (highest(r, n) - lowest(r, n))withr = rsi(x, n); unknown when that range is flat. - Price series: total return.
- Warm-up:
2n - 1bars. - Example:
stoch_rsi(close, 14) < 20
williams_r(n)
Williams %R, -100 to 0: how far the close sits below the top of the last n bars' range.
- Result: Percent.
- Formula:
-100 * (highest(high, n) - close) / (highest(high, n) - lowest(low, n)); unknown for a flat range. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
williams_r(14) < -80
cmf(n)
Chaikin money flow, -1 to 1: whether the volume traded nearer the high or the low.
- Result: Ratio.
- Formula:
sum(m * volume) / sum(volume)over the lastnbars, withm = ((close - low) - (high - close)) / (high - low), 0 for a flat bar. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
cmf(20) > 0.1
force_index(n)
Force index: price change times volume, smoothed, in dollars.
- Result: Dollars.
- Formula:
ema((close - close[1]) * volume, n). - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
force_index(13) > $0
percent_rank(x, n)
Where today's value ranks among the previous n, 0 to 100.
- Result: Percent.
- Formula:
100 * (number of the previous n values below today's) / n. - Price series: total return.
- Warm-up:
nbars. - Example:
percent_rank(close, 100) > 95
ibs()
Internal bar strength: where the close sits in today's range, 0 to 1.
- Result: Ratio.
- Formula:
(close - low) / (high - low); unknown when high equals low. - Price series: as traded.
- Warm-up: none.
- Example:
ibs() < 0.2
Volatility, ranges and bands
true_range()
Today's range, including any gap from yesterday's close.
- Result: Price.
- Formula:
max(high, close[1]) - min(low, close[1]). - Price series: split-adjusted.
- Warm-up: 1 bar.
- Example:
true_range() > 2 * atr(20)
atr(n)
Average true range with Wilder smoothing.
- Result: Price.
- Formula: seeded with the mean of the first
ntrue ranges, then(atr * (n - 1) + true range) / n. - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
atr(14) / close < 3%
stdev(x, n)
Population standard deviation of the last n values.
- Result: the unit of
x. - Formula: the square root of the mean squared distance from
sma(x, n). - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
stdev(close, 20) / close < 2%
zscore(x, n)
How many standard deviations today's value is from its n-bar mean.
- Result: Ratio.
- Formula:
(x - sma(x, n)) / stdev(x, n). - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
zscore(close, 20) < -2
volatility(n)
Annualised volatility of daily returns.
- Result: Ratio.
- Formula: the sample standard deviation of the last
ndaily log returns, times the square root of 252. - Price series: total return.
- Warm-up:
nbars. - Example:
volatility(60) < 30%
highest(x, n)
The highest value of the last n bars, today included.
- Result: the unit of
x. - Formula:
max(x[n-1], ..., x). - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close >= highest(close, 252)
lowest(x, n)
The lowest value of the last n bars, today included.
- Result: the unit of
x. - Formula:
min(x[n-1], ..., x). - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close <= lowest(close, 20)
donchian_high(n)
The highest high of the last n bars.
- Result: Price.
- Formula:
highest(high, n); use[1]to leave today out. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close > donchian_high(55)[1]
donchian_low(n)
The lowest low of the last n bars.
- Result: Price.
- Formula:
lowest(low, n). - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close < donchian_low(20)[1]
bollinger_upper(x, n, k)
The upper Bollinger band.
- Result: the unit of
x. - Formula:
sma(x, n) + k * stdev(x, n). - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close > bollinger_upper(close, 20, 2)
bollinger_lower(x, n, k)
The lower Bollinger band.
- Result: the unit of
x. - Formula:
sma(x, n) - k * stdev(x, n). - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close < bollinger_lower(close, 20, 2)
keltner_upper(n, k)
The upper Keltner band.
- Result: Price.
- Formula:
ema(close, n) + k * atr(n). - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
close > keltner_upper(20, 2)
keltner_lower(n, k)
The lower Keltner band.
- Result: Price.
- Formula:
ema(close, n) - k * atr(n). - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
close < keltner_lower(20, 2)
skew(x, n)
Skewness of the last n values, 0 when they are symmetric.
- Result: Ratio.
- Formula:
m3 / m2^1.5, wherem2andm3are the mean squared and cubed deviations from the mean; unknown for a flat window. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
skew(roc(close, 1), 60) < -0.5
ulcer(x, n)
Ulcer index, in percent: how deep and how long the last n values sat under their running high.
- Result: Percent.
- Formula:
sqrt(mean(dd^2)), wheredd = 100 * (v - peak) / peakandpeakis the highest value so far in the window. - Price series: total return.
- Warm-up:
n - 1bars. - Example:
ulcer(close, 14) < 5
max_drawdown(x, n)
The deepest fall inside the last n values, as a fraction (0 or negative).
- Result: Ratio.
- Formula: the lowest
v / peak - 1, wherepeakis the highest value so far in the window. - Price series: total return.
- Warm-up:
n - 1bars. - Example:
max_drawdown(close, 60) > -10%
correl(x, y, n)
Correlation of two series' daily changes over the last n days, -1 to 1.
- Result: Ratio.
- Formula: Pearson correlation of
x / x[1] - 1andy / y[1] - 1; unknown if either is flat. - Price series: total return.
- Warm-up:
nbars. - Example:
correl(close, close("SPY"), 60) < 0.5
beta(x, y, n)
How much x moves for each unit y moves, from their daily changes over the last n days.
- Result: Ratio.
- Formula: the covariance of the two daily changes over the variance of
y's. - Price series: total return.
- Warm-up:
nbars. - Example:
beta(close, close("SPY"), 252) < 0.8
Trend
macd_line(x, fast, slow, signal)
MACD line.
- Result: the unit of
x. - Formula:
ema(x, fast) - ema(x, slow). - Price series: split-adjusted.
- Warm-up:
slow - 1bars. - Example:
macd_line(close, 12, 26, 9) > 0
macd_signal(x, fast, slow, signal)
MACD signal line.
- Result: the unit of
x. - Formula:
ema(macd_line, signal). - Price series: split-adjusted.
- Warm-up:
slow + signal - 2bars. - Example:
crosses_above(macd_line(close, 12, 26, 9), macd_signal(close, 12, 26, 9))
macd_hist(x, fast, slow, signal)
MACD histogram.
- Result: the unit of
x. - Formula:
macd_line - macd_signal. - Price series: split-adjusted.
- Warm-up:
slow + signal - 2bars. - Example:
macd_hist(close, 12, 26, 9) > 0
adx(n)
Average directional index: how strong the trend is, whichever way, 0 to 100.
- Result: Percent.
- Formula: Wilder's directional movement:
+DIand-DIbelow,DX = 100 * |+DI - -DI| / (+DI + -DI), smoothed likeatr. - Price series: split-adjusted.
- Warm-up:
2n - 1bars. - Example:
adx(14) > 25
plus_di(n)
Plus directional indicator: the share of the range that was upward movement, 0 to 100.
- Result: Percent.
- Formula:
100 * smoothed(+DM) / smoothed(true range), where+DMis today's rise in the high when it beats the fall in the low, smoothed likeatr. - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
plus_di(14) > minus_di(14)
minus_di(n)
Minus directional indicator: the share of the range that was downward movement, 0 to 100.
- Result: Percent.
- Formula:
100 * smoothed(-DM) / smoothed(true range), where-DMis today's fall in the low when it beats the rise in the high. - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
minus_di(14) < 20
aroon_up(n)
How recently the highest high of the last n + 1 bars happened, 0 to 100.
- Result: Percent.
- Formula:
100 * (n - bars since that high) / n; the most recent of equal highs counts. - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
aroon_up(25) > 70
aroon_down(n)
How recently the lowest low of the last n + 1 bars happened, 0 to 100.
- Result: Percent.
- Formula:
100 * (n - bars since that low) / n. - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
aroon_down(25) < 30
slope(x, n)
The least-squares slope of the last n values: the change per bar.
- Result: the unit of
x. - Formula: the slope of the straight line fitted to
xagainst the bar number. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
slope(close, 90) / close > 0.1%
r_squared(x, n)
How well that straight line fits, 0 to 1.
- Result: Ratio.
- Formula: the coefficient of determination of the same fit; unknown for a flat window.
- Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
r_squared(close, 90) > 0.7
efficiency_ratio(x, n)
Net movement over the path travelled, 0 (choppy) to 1 (straight).
- Result: Ratio.
- Formula:
|x - x[n]| / sum(|x[i] - x[i-1]|)over the lastnchanges. - Price series: total return.
- Warm-up:
nbars. - Example:
efficiency_ratio(close, 10) > 0.5
supertrend(n, k)
Supertrend line: under the price in an uptrend, over it in a downtrend.
- Result: Price.
- Formula: bands
(high + low) / 2 ± k * atr(n)that only tighten while the trend lasts; the lower band in an uptrend and the upper in a downtrend, flipping when the close crosses it. - Price series: split-adjusted.
- Warm-up:
nbars. - Example:
close > supertrend(10, 3)
linreg(x, n)
Where the least-squares line through the last n values ends.
- Result: the unit of
x. - Formula: the fitted line's value at the newest bar;
linreg(x, n) + slope(x, n)is the next bar's. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
close > linreg(close, 50)
Bars and windows
lag(x, k)
The value k bars ago. x[k] means the same.
- Result: the unit of
x. - Formula:
x[k]. - Price series: that of its argument.
- Warm-up:
kbars. - Example:
lag(close > open, 1) and close > open
change(x, k)
The change over k bars.
- Result: the unit of
x. - Formula:
x - x[k]. - Price series: split-adjusted.
- Warm-up:
kbars. - Example:
change(close, 5) > 0
crosses_above(a, b)
True on the bar a moves from at or below b to above it.
- Result: Bool.
- Formula:
a > b and a[1] <= b[1]. - Price series: split-adjusted.
- Warm-up: 1 bar.
- Example:
crosses_above(close, sma(close, 50))
crosses_below(a, b)
True on the bar a moves from at or above b to below it.
- Result: Bool.
- Formula:
a < b and a[1] >= b[1]. - Price series: split-adjusted.
- Warm-up: 1 bar.
- Example:
crosses_below(close, sma(close, 50))
sum(x, n)
The sum of the last n values.
- Result: the unit of
x. - Formula:
x[n-1] + ... + x. - Price series: split-adjusted.
- Warm-up:
n - 1bars. - Example:
sum(close - open, 5) < 0
count(c, n)
On how many of the last n bars a condition held.
- Result: Count.
- Formula: the number of true bars in the window; unknown if any bar is unknown.
- Warm-up:
n - 1bars. - Example:
count(close < close[1], 5) >= 4
Daily, weekly and monthly
daily(x)
Evaluate x on daily bars: the completed days plus today so far. On daily data it is x.
- Result: the unit of
x. - Formula: see Intraday bars.
- Price series: that of its argument.
- Warm-up: that of
x, in days. - Example:
close > daily(sma(close, 20))
weekly(x)
Evaluate x on weekly bars: the completed weeks plus this week so far.
- Result: the unit of
x. - Formula: see Weekly and monthly bars.
- Price series: that of its argument.
- Warm-up: that of
x, in weeks. - Example:
weekly(close) > weekly(sma(close, 10))
monthly(x)
Evaluate x on monthly bars: the completed months plus this month so far.
- Result: the unit of
x. - Formula: see Weekly and monthly bars.
- Price series: that of its argument.
- Warm-up: that of
x, in months. - Example:
monthly(roc(close, 9)) > 0
Across securities
rank_pct(x)
Percentile of x among the universe on each date, 0 to 100.
- Result: Percent.
- Formula: lowest 0, highest 100; ties share their average rank.
- Price series: that of its argument.
- Warm-up: that of
x. - Example:
rank_pct(roc(close, 60)) > 90
cs_zscore(x)
How many standard deviations x is from the universe's mean on each date.
- Result: Ratio.
- Formula:
(x - mean) / population standard deviationacross the universe. - Price series: that of its argument.
- Warm-up: that of
x. - Example:
cs_zscore(roc(close, 20)) > 1
bucket(x, k)
Which of k equal-sized groups x falls in on each date, 1 (lowest) to k.
- Result: Count.
- Formula: sort the universe by
x; ties go to the lower security id first. - Price series: that of its argument.
- Warm-up: that of
x. - Example:
bucket(roc(close, 60), 10) == 10
Arithmetic
abs(x)
Absolute value.
- Result: the unit of
x. - Formula:
abs(x). - Price series: that of its argument.
- Warm-up: none.
- Example:
abs(roc(close, 1)) < 5%
min(a, b)
The smaller of two values, which need the same unit.
- Result: the common unit of
aandb. - Formula:
min(a, b). - Price series: that of its argument.
- Warm-up: none.
- Example:
min(open, close) > sma(close, 50)
max(a, b)
The larger of two values, which need the same unit.
- Result: the common unit of
aandb. - Formula:
max(a, b). - Price series: that of its argument.
- Warm-up: none.
- Example:
max(open, close) < sma(close, 50)
Your data
series("name")
Your own number per security and day, from series/name.csv of your data.
- Result: Number.
- Formula: the latest value dated on or before the day; see Your own data.
- Price series: as traded.
- Warm-up: none.
- Example:
series("pe_ratio") < 15
Lookback
Each script has a lookback: how many trading days before a signal its inputs can reach, with every param at the top of its range. Windows add up when nested (sma(rsi(close, 14), 20) reaches 34 days), x[k] adds k, and a weekly bar counts as five days and a monthly bar as 23. On intraday data a window reaches back in bars and daily, weekly or monthly in days, so rvw check --json gives the two parts as lookback_bars and lookback_days. The lockbox uses the lookback plus the holding period to size the gap it leaves before the held-back data.
Limits
- Nesting is limited to 64 levels.
- An expression or universe may hold at most 400 operators.
- Whole numbers are limited to 2^53.
- Counts (
max positions,max bars,max adds,bars_held >= N,rebalance every N days,keep top N) go from 1 to 1,000,000. - A param's range holds at most 100,000 steps:
0..100000 step 1is allowed,0..100001 step 1isn't.
Checking a script
The checker reports every problem it finds, not just the first. Each diagnostic has a line, a column, a severity, a code and a message.
| Code | Meaning |
|---|---|
UNEXPECTED_CHAR | a character that belongs to no token |
UNTERMINATED_STRING | a string with no closing " on its line |
BAD_NUMBER | a number too large |
BAD_TIME | a time not written HH:MM from 00:00 to 23:59 |
MISSING_VERSION | the file doesn't start with language 1 |
UNSUPPORTED_VERSION | a language version other than 1 |
UNEXPECTED_TOKEN | any other syntax error; the message says what was expected |
CHAINED_COMPARISON | a < b < c; write a < b and b < c |
BAD_OPTION | an unknown word after fill, entry or same_bar |
TOO_COMPLEX | nesting or operators over the limits |
UNKNOWN_IDENT | a name that is no binding, param or series |
UNKNOWN_FUNCTION | a call to a name that is no function |
DUPLICATE_BINDING | a name defined twice |
RESERVED_NAME | a binding or param named like a keyword or built-in series |
CYCLE | bindings that depend on themselves |
UNIT_MISMATCH | units that can't be combined |
NOT_BOOL | a number where a condition is needed |
NOT_NUMERIC | a condition where a number is needed |
ARITY | the wrong number of arguments |
BAD_ARG | an argument of the wrong kind or out of range |
BAD_LAG | an index that isn't a whole number from 0 to 10,000, written out |
BAD_STRING | an empty label, or a ticker or series name that isn't valid |
STRING_NOT_ALLOWED | a string where only a number or series makes sense |
BARS_HELD_MISUSE | bars_held anywhere but a bars_held >= N exit term |
MISSING_UNIVERSE, MISSING_ENTRY, MISSING_SIZE | a required statement is missing |
DUPLICATE_STATEMENT | a statement given twice |
BAD_VALUE | a percentage, count or amount out of range |
SIZE_NEEDS_ATR_STOP | size risk without an ATR stop |
CONFLICTING_MAX_BARS | max bars and bars_held >= N disagree |
BAD_PARAM_RANGE | a param range that is empty, uneven, has a step not above 0 or more than 100,000 steps, or doesn't hold the default |
BAD_SETTING_PARAM | a param that sets a statement but whose range doesn't suit it, or a binding where a statement needs a number |
REBALANCE_CONFLICT | rebalance with add when, scale out, or limit or stop entries |
SCALE_OUT_BEYOND_TARGET | scale out ... at a percentage not below the target |
KEEP_BELOW_MAX_POSITIONS | rebalance ..., keep top N with N below max positions |
SCALE_OUT_LADDER | a scale-out ladder whose levels don't rise, more than 8 steps, or max scale outs without scale out ... when |
SCAN_STATEMENT | a statement that trades, such as size or enter, in a scan |
SCAN_COLUMN_NAME | a scan binding named like one of the scan's own columns, such as score or ticker |
UNKNOWN_PARAM, BAD_PARAM_VALUE | a run asked for a param the script lacks, or a value outside its range |
DEFAULT_MAX_POSITIONS | warning: no max positions, so 10 is used |
NO_EXIT | warning: nothing closes a position before the data ends |
SAME_CLOSE_APPROX | warning: fill same_close results are approximate |
PARAM_OFF_GRID | warning: a param default not on its grid |
UNUSED_BINDING, UNUSED_PARAM | warning: something nothing refers to |
MAX_ADDS_UNUSED | warning: max adds without add when |
In the editor
The editor helps while you write.
- Completion. As you type a word, a list offers what fits there. At the start of a line it offers the statements, and after a statement's first word its other words (
filloffersnext_open,next_closeandsame_close). In an expression it offers your bindings and params first, then the functions with their signatures, the series andnot. Inside quotes it offers names from the open data: tickers inclose("…")andsymbols("…"), index labels inmembers("…"), sectors, industries, and your series inseries("…"); behind a sealed lockbox, none whose data starts after it. ↑ and ↓ choose, Return or Tab accepts, and Esc closes the list, or shows it when none is open. A new binding's name never turns into a statement: the list closes at the space after it. - The argument you're on. Inside a call's parentheses, a tip above the line shows the call with the argument you're typing in bold, and what that argument takes.
- Quick Help. Rest the pointer on a word, ⌥-click it, or choose Editor ▸ Show Quick Help (⌃⌘?) for what it is: a function's formula, price series, warm-up and an example, with Open in Help for its entry on this page; a binding's definition, unit and its figures in the last run; a param's range; a statement's forms. A problem the check found there shows at the top.
- Jump to Definition. ⌘-click a binding or param, or choose Editor ▸ Jump to Definition (⌃⌘J), to select the line that defines it. On a function it opens the function's entry here.
What each line did
Beside the script, the app shows what each statement did over the run. The figures count member-days: a security on a day of the run when it is in the universe, so a candidate for an entry that day.
| Statement | Figure |
|---|---|
universe | securities in it per day: the average and the range |
filter | the share of member-days it passed |
| A condition binding | the share of member-days it was true |
| A number binding | its median, its 10th and 90th percentiles, and a histogram of its values on member-days |
enter long when, enter short when | signals: the candidates the engine could place, before rank by picks among them |
add when | signals |
rank by | crowded days: days with more signals than open slots (max positions less the positions open at that day's close), when the ranking chose |
size, max positions | positions open at the close: the average and the most |
fill | the median entry fill against the signal day's close |
| Exits, stops and targets | the trades each one closed, from the trades |
A binding read only as part of another ticker's expression, such as c in c close("SPY") and regime c > sma(c, 200), shows nothing of its own. The binding that uses it shows the figure. Above 50,000 member-days, the median, percentiles and histogram come from an evenly spaced sample of them. The figures are the same whatever the number of threads, and they never change the run: rvw run --lines adds them to a run folder as lines.json.
Writing scripts with an AI agent
Rearview makes no AI calls of its own. If you use an AI coding agent, point it at this file and at the checker. Then it can write a script, check it and fix it without you.
- Give the agent this reference. In Rearview, File ▸ Save Language Reference to Project writes it into the project folder as
rearview-language.md. An agent opened in that folder finds it next to the scripts. - Give it the checker. The command-line tool is inside the app at
Rearview.app/Contents/Helpers/rvw; Help ▸ Copy Command-Line Tool Path copies the full path. - Let it loop: write the script, run
rvw check <script.rvw> --json, fix every error, and check again until"ok"is true.
rvw check --json prints one object:
{
"ok": false,
"diagnostics": [
{"severity": "ERROR", "code": "UNIT_MISMATCH",
"message": "cannot compare Price with Percent; both sides need the same unit",
"span": {"line": 4, "col": 19, "start": 59, "end": 80}}
],
"strategy": null, "params": null, "lookback": null, "max_bars": null, "fill": null,
"setting_params": null, "rebalance": null, "adds": null, "scale_out": null,
"kind": "strategy"
}- The result:
okis true when no diagnostic is an error; warnings don't stop a run. - Where a problem is:
lineandcolstart at 1. - When
okis true:strategyis the script's name,paramslists each param with its range,lookbackis the lookback in trading days, andmax_barsandfillare the holding limit and fill timing.setting_paramslists the params that set a statement.rebalancegives the period and its length in trading days,addsthe most adds per trade, andscale_outthe share sold and its partial target (fractions); each is null when the script doesn't use it. - What the file is:
kindisstrategyorscan, from its header, even when the check fails. - Exit code: 0 when there are no errors, 1 otherwise.
To try a script, run it in its project:
rvw run --project <project folder> --script <script.rvw>--script is the script's path in the project, such as rsi2_pullback.rvw. The run is the one Run makes in the app: on the project's data, with its run settings, the script's saved params and its lockbox. It goes into a new folder in the project's runs/, which holds the trades, the daily equity and a summary.json of the metrics. The run is also added to the project's tries log, so the luck figures count it like any other try (see Every try counted): a hundred variations an agent tries count as a hundred tries.
rvw search, rvw walkforward and rvw scan take --project the same way, and a search or walk-forward goes into searches/. The project decides the data, the settings, the params, the lockbox and where the folder goes, so --store, --out, --param, --config and --lockbox-start can't be given with it; a scan still needs --out, since it is no try. To see what the project has run:
rvw runs --project <project folder>It lists the runs, newest first, each with its script, when it ran, and its CAGR, Sharpe ratio, deepest fall and trades.
A run outside a project, rvw run --store <data store> --script <script.rvw> --out <folder>, is counted nowhere. The data store of each library entry is under ~/Library/Application Support/Rearview/Data/<name>/store.
The command-line tool shares the app's licence and trial. rvw licence says which applies; rvw licence install <file> takes a licence as the app's Rearview ▸ Licence… does. Once the trial has ended without a licence, rvw run, rvw search, rvw walkforward, rvw scan, rvw signals and rvw eval say so and stop, while rvw check, reports and exports of saved runs keep working.
To see what an expression computes, day by day, for a few tickers:
rvw eval --store <data store> --expr "rsi2=rsi(close, 2)" --expr "trend=sma(close, 200)" --tickers AAPL,MSFT --out values.csvIt writes date,ticker,rsi2,trend with one row per ticker and day, and an empty value where the expression is unknown, such as during an indicator's warm-up. An expression must be a number; to look at a condition, look at the numbers it compares.
To see why a run did or didn't enter a security on a day, trace it:
rvw trace <run folder> --store <data store> --ticker AAPLIt prints, for each day of the run, whether the security was in the universe, each binding's value, each condition (true, false, or null when unknown), the rank score, and the days its entries could be placed and its exits were signalled. --ticker means the security holding that ticker on the run's last day, --id names one by its id, --strategy picks a portfolio's script, and --out writes the JSON to a file. It is what the app's Chart tab draws.
To see what the run did with each candidate at a close, ask for its choices:
rvw choices <run folder> --store <data store> --date 2025-06-12
rvw choices <run folder> --store <data store> --ticker AAPLThe first lists every entry candidate at that day's close: its place in the ranking, the free slots, and what became of it, with its order's status and fills when one was made (see The day's choices). The second gives the security's outcome at every close it was a candidate. Without --date, --id or --ticker it is the run's last close, the one the next-day orders came from. --summary adds every close up instead, to say why a run made few trades or none: how many candidates were dropped before ranking, ranked, ordered and not, and why, with a headline such as "All 412 candidates were sized to zero." The run is simulated again, which takes as long as the run did, and must make the orders its folder holds.