Getting Started

To start, simply write a few stocks, ETFs or mutual fund symbols and press "Go"


This will instantly simulate a Buy and Hold strategy for that symbol including reinvestment of dividends.


It is possible to mix several symbols with weights, in a passive portfolio. The numbers after each symbol indicate the percentage weight.



All portfolios are rebalanced yearly by default. We can change the rebalancing period to be none / daily / weekly / monthly / quartely / yearly.

rebalance monthly

We can also test multiple rebalancing periods in the same query

VFINX:60|VFITX:40 { rebalance daily }
VFINX:60|VFITX:40 { rebalance weekly }
VFINX:60|VFITX:40 { rebalance monthly }
VFINX:60|VFITX:40 { rebalance quarterly }
VFINX:60|VFITX:40 { rebalance yearly }

Tip: compare the above query with and without friction (as detailed below) to see the impact of taxes and rebalancing frequency on returns.

We can also rebalance based on a relative or absolute drift from the portfolio target allocation:

VFINX:60|VFITX:40 { rebalance { none; abs 5 } }
VFINX:60|VFITX:40 { rebalance { none; rel 20 } }
VFINX:60|VFITX:40 { rebalance { none; abs 5; rel 20 } }

Note that if we want to rebalance only based on absolute / relative drifts, we should specify 'none' as the rebalancing period.

The default rebalancing config is yearly.


The portfolio weights don't have to sum to 100 (%), we can have more than 100% for leverage of less than 100% for a partial cash position

SPY:100|BND:50 // this portfolio has a 1.5 leverage
SPY:25|BND:25  // this portfolio has a 0.5 leverage (or a 50% cash position)

We can also explicitly state the leverage:

// both of these portfolio are the same - both have 25% SPY, 25% BND and 50% cash              
SPY:50|BND:50 { leverage 0.5 }

Start / End Dates

We can specify specific start / end dates:

start '2009/3/14'
start '2009/3/14'
end '2013/1/1'

The date is specified as 'year/month/day'

Deposit / Withdraw

We can make one time or peridoc deposits and withdrawal of cash:

deposit { amount 1000; every 1month }

We can also specify when to start the deposit and for how long:

deposit { amount 1000; every 1month; after 5y; for 10y }

Or create a one-time deposit:

deposit { amount 10000; after 1y }

Note that the 'every', 'after' and 'for' configs expect a Period value.


Periods are repeating intervals of time such as a day or a month. Examples of periods are: 1month, 2days, 5y, 10w, 2quarters. Each period starts with a number and is followed by the period name or its initial letter.

The same goes for withdraw:

withdraw { amount 1000; every 1y; after 5y; for 10y }

We can mix both of them together:

deposit { amount 1000; every 1month; for 10y }
withdraw { amount 5000; every 1q; after 10y }

Note that there can only be 1 schedule for deposits and withdrawal each.
Support for inflation adjustment and percentage withdrawal is in progress.


In the real world, investing incurs friction costs, such as taxes, trading fees, management fees, bid-ask spreads, slippage, margin costs and more. In retrolyzer these costs are accounted for by default. To turn them all off add 'friction off' to the query, or you could disable/enable any one of them using taxes/fees/slippage/wholeUnits flags with on/off values.

friction off // turn off all friction costs
friction off // turn off all friction costs
fees on // but keep fees costs

Configuration flags apply to the entire query, but we can also apply them on a per-strategy basis

SPY // default, has all friction costs
SPY { friction off } // has no friction costs
SPY { fees off } // has no fees costs, but left with taxes and slippage costs
SPY { fees off; slippage off } // has only taxes costs
SPY { friction off; taxes on } // also has only taxes costs
SPY { fees off; slippage off; taxes off } // same as 'friction off'
tradeFees: When on, accounts for fees related to executing a trade.
taxes: When on, accounts for dividends and capital gains taxes and their (or lack of) withholding.
slippage: When on, accounts for the bid-ask spread using a non-linear model based on the volume of a stock at the time of transaction.
wholeUnits: When on, any purchase or sell of stock must be in whole units, meaning imperfect usage of cash and weights allocation.

We can also configure some of them at a more granular level:

tradeFees { min 1; stock 0.005; percent 0 } // default

min - minimum cost per trade; stock - cost per stock unit traded; percent - a percent of the total trade value deducted as a fee.

taxes { divs 25; gains 25; withhold divs } // default

divs - dividend tax percent; gains - capital gains tax percent; withhold - which taxes are withheld, can be divs/gains/both. When withhold is active for a specific tax type, the gains/dividends will be after-tax at the moment of the tax event. When it's inactive, the liable tax will be payed at the end of the year.


When a portfolio has capital gains, usually the investor caries a tax libility for those gains, meaning that the net post tax libility value of the portfolio is lower than its market value. By default, all metrics are based on the market value of the portfolio, i.g. pre-liquidation, but by setting 'liquidation on' we can have all metrics, including the equity curve, CAGR, StdDev etc, be based on the post-liquidation value.

SPY { liquidation on }

This is useful when comparing strategies which might have different tax liabilities, and comparing their market value will paint a skewed image of their post-tax value.

Math Operations

Any time-series can be transformed by math operations, which include: * / + -

SPY; SPY * 2

Math operation can involve both time-series and constant numbers.
Note that we can write multiple statement on the same line by seperating them with ';'.


We can divide one portfolio over another to compare their equity charts on a relative basis

SPY:25|TLT:25|GLD:25|SHY:25 / SPY:60|BND:40

Note that this is in fact just a simple devision operator, which can be applied on any combination of time-series. What makes this unique is that unlike a comparison of the VTI ticker history vs that of SPY, here we compare the full equite chart of buy and hold portfolios of these ETFs which includes accounting for taxes, fees and all friction costs which affect the effective outcome.


Function allow to transform any time-series to another. A basic example is a Moving-Average

ma(SPY, 50)
ma(SPY, 200)

Function can be applied on any time series

ma(ma(SPY, 50), 20)
ma(VTI / SPY, 100)

Note that function on time-series always return a time-series.
The current set of builin functions include:

Builtin Functions

See builtins.bql for the full list of builtin functions with their code and documentation.


We can store values in variables and use them to simplify queries and avoid repetition.

classic = SPY:60|BND:40              
harryBrown = SPY:25|TLT:25|GLD:25|SHY:25
n = 100
classic; ma(classic, n)
harryBrown; ma(harryBrown, n)
classic / harryBrown

Numeral and Vectoral Time-Series

Almost everything in BQL is a time-series. Most time-searies are numeral, that is for every date there is a single numeral value. BQL also supports vectoral-time-series, where for every date there is a vector (array) of numeral values. Vectoral-time-series are usefull for certain kinds of functions and strategies where a lookback window is desired. See for example usage of the cor() function defined below.

Custom Functions

We can define our own functions, both to simplify queries with repeating logic and to add new functionality.

func div(a, b) {
  a / b
div(VB, SPY)

Simple single-statement function can also be written in short hand as:

func mul(a, b) => a * b
mul(SPY, SH) // an example of the hidden costs of short ETFs

We can even define ma() as a custom BQL function. While the actual builtin ma() is written in native code for maximum efficiency, this BQL implementation is also a builtin function named _ma(), and can be used the same way as ma(). Note that BQL allows overriding builtin functions with custom ones.

// moving average (we have a more efficient ma() func in non BLQ code)
func ma(x, n) => sum(take(x, n)) / n
ma(SPY, 100)

A more useful example would be an implementation of a correlation function. In fact, this function is a builtin function written in BQL and is evaluated at run time as is.

// rolling correlation between x and y over the past n trading periods (days)
func cor(x, y, n) {
  x = ret(x)
  y = ret(y)
  a = take(x, n) - ma(x, n)
  b = take(y, n) - ma(y, n)
  sum(a * b) / sqrt(sum(a * a) * sum(b * b))
cor(SPY, BND, 100)

Another example is a drawDown() function, which like cor() is a builtin function written in BQL.

func drawDown(x) {
  ((x - max(x)) / max(x)) * 100


The basic Buy and Hold strategy is nice, but to achive more we want to use Strategies and Signals.

when SPY > ma(SPY, 50) hold SPY

The above strategy will hold SPY only when its value is above its 50 day moving average, otherwise it will hold cash.
Or we can be more explicit and choose what to hold otherwise

when SPY > ma(SPY, 50) hold SPY else BND:50|GLD:50

We can also view the signal itself, which translates into a binary 0/1 signal.

SPY; ma(SPY, 200); SPY > ma(SPY, 200)

For Loops

We can iterate over a range of values to cover more ground much faster.

for i in 1..10
when SPY > ma(SPY, i * 10) hold SPY

This is equivalent to writing 10 strategies with different lengths for the ma() function.
Note that a for-loop can only iterate a single statement at the moment.

We can even skip the entire for-in part and iterate implicitly:

when SPY > ma(SPY, 1..10 * 10) hold SPY

This iterates the entire statement as if it was in a for-loop, replacing the specified range with the value of the iteration.

Auto Optimization

Suppose you want to find the optimal value of a parameter for a strategy like this one:

when SPY > ma(SPY, ?) hold SPY

Well you can! Running the above query will iterate the strategy with different values for the free variable '?' using a simulated annealing algorithm to find an approximate optimum quickly.

We can also limit the search to a specific range, or suggest the search to start from a specific value:

when SPY > ma(SPY, 1..100?) hold SPY
when SPY > ma(SPY, 200?) hold SPY

- The number of iteration for each optimization is limited to 50.
- Due to the random nature of the simulated annealing algorithm, results are likely to vary between repeating executions.
- Only a single free variable can be optimized at the moment.
- By default, the algorithm optimizes for returns (cagr). We can choose between cagr/stddev/maxdd/sharp (all lower case) using the optimize config:

(when SPY > ma(SPY, ?) hold SPY) { optimize sharp }
(when SPY > ma(SPY, ?) hold SPY) { optimize cagr }

Trim and Align

By default, strategies with different starting dates will all start from a common starting date which allows them all to be compared on even grounds. The downside of this is that we don't see what went on before that date, even though the data exists.


We can disable this behaviour by setting 'trim off'

trim off

While this allows to see the values of each strategy before the common starting date, it also causes the strategies to be on uneven ground as each one began on a different date, and their performance metrics will also depend highly on those dates.

trim off; align off

Even when trim is off, by default the strategies will be aligned to start at the same point as the common starting date. This will cause their dollar value to be skewed; We can disable aligment using 'align off'.