Strategies API

DocumentationWriting strategies

Strategies API

A Python strategy subclasses Strategy and implements on_bar(self, ctx), called once per completed bar. The context releases data with no lookahead and is the only surface a strategy may touch.

Hooks

  • on_bar(self, ctx) is the per bar hook. on_market(self, ctx) is the name this engine used first and is still called, so a strategy already written against it keeps running; the seeded examples in the app are written that way. Define both and on_bar is the one called. Define neither and the run stops with a message naming the class.
  • on_stop(self) runs once after the run, with finish(self) as its earlier name. A strategy that defines both gets both called. Neither receives the context, so this is for cleanup, not for a last trade.
  • on_news(self, ctx, event_type, importance) exists on the base class and the engine calls it when a news event is released, but nothing in a run currently releases one. Economic events reach a strategy through ctx.news_data instead.

The context

  • ctx.bars(n) returns up to the last n bars, oldest first. Each bar has .open .high .low .close .volume .ts. A run holds one instrument, so asking for another symbol returns an empty list.
  • ctx.now() is the timestamp of the most recently released bar.
  • ctx.symbol.name is the instrument being traded, ctx.symbol.pip_size its pip size.
  • ctx.symbol also carries tick_size, contract_size, asset_class, quote_currency and price, and ctx.account carries balance, equity, starting_balance and currency. Both are read only. A position sized by the balance says so on its signal: see The size of a position.
  • ctx.indicators.sma(period), .ema(period), .rsi(period), .atr(period) and .custom(name) return the latest streamed value, or None until warmed up. The moving averages and RSI take a second argument, source, one of close, open, high or low, defaulting to close. ATR reads the whole bar.
  • ctx.sma(...), ctx.ema(...), ctx.rsi(...), ctx.atr(...) and ctx.custom(...) are the same indicators under the older names and return the same values.
  • ctx.ind(id, **inputs) reads any indicator of the library by its id, with its inputs by name; one left out takes its default. One line gives a number, or None until warmed up: ctx.ind("rsi", length=14). Several lines give a reading with each line by name, None while it warms up, and .value for the first: ctx.ind("bb", length=20).upper. An unknown id or input stops the run with a message naming it. ctx.indicators.ind(...) is the same call. See Indicators API.
  • ctx.market_structure(**inputs) reads the whole market structure group at once: ms.trend, ms.bos, ms.choch, and each element by name, such as ms.fvg.bull_top or ms.fib.level_618. Each element is its ctx.ind read.
  • ctx.pip() is the instrument's pip size, the same number as ctx.symbol.pip_size. What it means per asset class is the next section.
  • ctx.custom(name) reads a custom indicator formula configured on the run.
  • ctx.read("trend.pine", "up") is a value another file of the strategy's workspace gives on this bar: a Pine file's top level declaration, a canvas's Output block, the dict a Python file's outputs(ctx) returns, what a C# Outputs class Sets, what a Rust file ctx.sets. A number, true as 1.0 and false as 0.0, or None while the file has no value yet. Write the file and the name in quotes, so the engine finds the read and runs that file beside this one. A file that gives values places no orders and reads no other file.
  • ctx.bid and ctx.ask are the best bid and ask at the close of the current bar, and ctx.bid_size and ctx.ask_size the size offered at each, in the data vendor's unit (millions of units of the base currency for forex). All four come from the last tick of the bar that just closed, so they are never later than the bar you are reading, and they are None below the TICK tier, where there is no quote to give. The sizes are also None on ticks cached before they were kept: fetch the days again to get them.
  • ctx.depth(levels) is the order book at the same moment, on a tick run: bids and asks, best level first, each with a price in this instrument's own prices and a size in instrument units (lots times the contract size). There is no L2 choice to make: a tick run whose strategy reads the book in code gets the depth on its own. It needs order book files in the cache. The desktop app downloads them before such a run with a Databento key of your own, set under Settings, API keys, and Databento bills you for them. Without a key, or for an instrument no exchange book is chosen for, it returns None, the same as on a run below the tick tier. Write for None. A strategy that reads the book exports to a MetaTrader 5 expert advisor, with MarketBookAdd and MarketBookGet, and to a cTrader cBot, with MarketData.GetMarketDepth. The file reads the book when the new bar's first quote arrives, where this engine reads it at the bar's last tick. TradingView has no order book, so the export to it is refused at the line that reads it. The sizes at the touch, ctx.bid_size and ctx.ask_size, are in the data vendor's unit and export nowhere.
  • ctx.plugin_indicator(plugin_id, name, params) returns the live indicator object a plugin registered, or None when the plugin or indicator is unknown or disabled. See Plugins API.
  • ctx.news_data is the list of economic events when the run flag is enabled, and ctx.news_data_problem says what the list lacks for the run, None when nothing.
  • ctx.data(name, date) reads a plugin data feed by date (see External Data).
  • ctx.series("ustreasury/par.10y") reads a Terminal series as it was known at the bar (see Terminal).
  • ctx.signal(direction, sl_pips=..., tp_pips=..., sl_percent=..., tp_percent=...) expresses intent, where direction is "LONG", "SHORT", or "EXIT". Orders fill on the next bar, which keeps the run free of lookahead. A stop that moves and the position's size are more arguments, each in a section below.
  • ctx.order(direction, limit=... or stop=..., sl_pips=..., tp_pips=..., bars=..., name=..., oco=...) rests an entry order at a price instead; ctx.cancel(name) takes one back. See the next section.
  • ctx.close() closes what is open, as ctx.signal("EXIT") does, and ctx.close(0.5), ctx.close(lots=...) or ctx.close(units=...) closes part of it. See Closing part of a position below.
  • ctx.spread(back=0) is the ask less the bid at the close of the bar back bars ago, in price: divide by ctx.pip() for pips. A bar without an ask side gives the average spread of a custom symbol's range; without one, reading it stops the run with the reason, as a fill without a spread does.
  • ctx.position is what the engine holds once this bar's fills are done, None when flat: its direction ("LONG" or "SHORT"), entry_price, stop_price (None without a stop; a moving stop where it stands now) and bars, the bars it has been open, 1 on the bar it filled on. What you ask for on this bar changes it from the next bar on. In an export, MetaTrader 5 and cTrader read their own position and TradingView its own, all but the stop, which Pine cannot read.
  • A bar's bid and ask are bar.bid_open, bar.bid_high, bar.bid_low and bar.bid_close, and the same four for ask: None on data without an ask side, where bar.sides is None. The MetaTrader 5 and cTrader exports read them from the quotes the expert advisor or the cBot sees; TradingView's bars have one price, so its export is refused at the line that reads one. Rust reads the same as ctx.position() and ctx.bar().ask_low(), C# as Position (null when flat, StopPrice NaN without a stop) and Bars.AskLow, NaN without sides.
  • ctx.news_window("high", before=30, after=15) is true when the economic calendar has a release of at least that impact for the instrument's currencies from 15 minutes before the bar's open to 30 minutes after it. currencies=["USD"] names others. See News windows below.

News windows

A news window keeps a strategy out of the market around economic releases. Ask with ctx.news_window(impact, before, after): impact is "low", "medium" or "high", and counts that level or higher. The currencies are the pair's two, or the quote currency of a share or an index, unless you name them. The bar's time is its open. An order fills at the next bar, so add one bar to before to keep an entry out of the window.

kinds narrows a window to some kinds of release: "rate" (central bank rate decisions), "jobs" (US payrolls, the employment change of Australia, New Zealand and Canada, the UK claimant count) and "cpi" (consumer prices). ctx.news_window("high", before=600, after=120, currencies=["EUR", "GBP"], kinds="rate") keeps a strategy out from 10 hours before an ECB or Bank of England decision to 2 hours after it. Left out, every release of the impact counts.

The app loads the calendar from ForexFactory on your computer before the run, one week at a time, and keeps the weeks it has loaded. ForexFactory gives a program only the current week now, so where it has no week, the official release calendar that comes with the app stands in: the rate decisions, jobs reports and consumer prices of EUR, GBP, USD, AUD and NZD from 2016 to the end of 2026, read from the central banks and statistics offices. It answers a window of high impact that names those kinds and currencies. A server run gets the calendar from the app that starts it. If the calendar cannot answer a window, the run stops with news_missing and the reason. It never trades on as if there were no news. Rust strategies ask with ctx.news_window(Impact::High, 30, 15) and ctx.news_window_of(Impact::High, &[NewsKind::Rate], &["EUR"], 30, 15), C# with NewsWindow("high", 30, 15) and NewsWindowOf("rate", "high", 30, 15, "EUR"), a signal strategy and a cBot alike. The MetaTrader 5 export reads MetaTrader's own calendar. TradingView and cTrader have none, so their exports refuse it.

Orders at a price

ctx.signal enters at the next bar's open. ctx.order waits for a price:

  • limit=price buys at that price or lower, or sells at it or higher: a pullback.
  • stop=price buys once the market trades at that price or higher, or sells at it or lower: a breakout.
pythondef on_bar(self, ctx):
    bar = ctx.bars(1)[-1]
    if self.breakout_setup(ctx):
        ctx.order("LONG", stop=bar.high + 2 * ctx.pip(), sl_pips=20, tp_pips=40,
                  bars=5, name="up", oco="range")
        ctx.order("SHORT", stop=bar.low - 2 * ctx.pip(), sl_pips=20, tp_pips=40,
                  bars=5, name="down", oco="range")
  • An order rests from the next bar until it fills, bars bars have passed, ctx.cancel(name) takes it back, or another one of its oco group fills. ctx.cancel() with no name takes back every order. A new order with the name of one already resting replaces it.
  • A limit fills at its price, or at the open when the bar opened past it. A stop fills at its price, or at the worse open of a bar that gapped through it. A buy is checked against the ask and a sell against the bid, on bars that carry both. At the TICK tier the fill is the quote that reached it: the ask for a buy, the bid for a sell. At the L2 tier a limit also waits for its place in the queue at its price, and fills at its price.
  • A fill opens a position of the order's size, its stop and target measured from the fill price. Holding the other way, it closes that position at the same price and opens the new one. Holding the same way, the order waits.
  • Inside a bar, the stop or target of what was already open comes first. A position an order opens has its stop and target checked from the next bar, as one opened at the open does. That flatters a limit bought into a bar that kept falling, so read a limit strategy on M1 or TICK before trusting it.
  • A trade opened this way says so: entryOrder is "limit" or "stop" in the result.
  • Exports to TradingView, MetaTrader 5 and cTrader do not carry orders at a price yet, and refuse such a strategy with the line of the first one.

A stop that moves

ctx.signal and ctx.order take four more distances, in pips or, with _percent instead of _pips, as a percentage of the last close:

  • trail_pips: the stop follows the best price since the entry at this distance, and only toward profit. Without a start it stands this far behind the fill at once.
  • trail_start_pips: the trail starts once the best price is this far in profit.
  • break_even_pips: once the best price is this far in profit, the stop moves to the entry, plus break_even_lock_pips in your favour.
pythonctx.signal("LONG", sl_pips=25, tp_pips=80, trail_pips=15, trail_start_pips=20)

The stop is the best of the one you set, the trail and break even. From the bar after the entry, each part of a bar (the bar on OHLC, each minute on M1, each quote on TICK) is checked against the stop as it stood, and then moves it by its high for a long or its low for a short, read on the bid for a long and the ask for a short where the bar has both. Which of a part's high and low came first is not known, so a move never counts inside the part that made it; on M1 and TICK the stop follows the path closely. Exports do not carry a moving stop yet, and say which line stops them.

The size of a position

Every entry uses the run's lot size unless ctx.signal or ctx.order says otherwise, with one of these:

  • lots=0.5: lots.
  • units=20000: units of the instrument; the contract size is how many make a lot.
  • cash=5000: the position's value in the account's money, at the last close.
  • size_percent=20: that value as a percentage of the balance.
  • risk_percent=1: the percentage of the balance the stop loses if it is hit. It needs a stop.
pythonctx.signal("LONG", sl_pips=25, tp_pips=50, risk_percent=1)

The size is measured when the signal becomes an order: with the balance after the bar's fills and the bar's close. A reversal is sized before the position it reverses is closed, so that trade's profit counts from the next entry. A size that comes to no positive number of lots, or a risk with no stop, does nothing, not even the close of a position held the other way. Exports do not carry a size yet, and say which line stops them.

Closing part of a position

ctx.close takes one of three amounts:

  • ctx.close(0.5), or fraction=0.5: a share of the position, here half of it.
  • lots=0.2: that many lots.
  • units=10000: that many units of the instrument.
pythonif self.first_target_reached(ctx):
    ctx.close(0.5)

The amount is measured on what is held when the signal becomes an order, and the part fills at the next bar's open like every exit. As much as is held or more closes all of it, and an amount that comes to no positive number of lots closes nothing. The rest stays open with its entry, its stop, its target and its moving stop. The part is a trade of its own in the result, with the position's id and entry, its share of the entry's commission and its own exit. Exports to TradingView, MetaTrader 5 and cTrader do not carry it yet, and say which line stops them.

Many positions at once: a hedging account

A strategy that sets hedging = True on its class trades like an account at a broker that hedges: as many positions as it opens, in both directions, each with its own stop, target and label, filled the moment it asks.

pythonclass Grid(Strategy):
    hedging = True

    def on_bar(self, ctx):
        held = ctx.positions
        if not held:
            ctx.open_position("LONG", 0.1, tp_pips=15, label="buy")
            ctx.open_position("SHORT", 0.1, tp_pips=15, label="sell")
            return
        buys = [p for p in held if p.label == "buy"]
        if buys and buys[-1].pips <= -15:
            ctx.open_position("LONG", buys[-1].lots * 2, label="buy")
            for p in buys:
                ctx.modify_position(p.id, tp=None)
  • ctx.open_position(direction, lots, sl_pips=..., tp_pips=..., label=...) opens now, at the close's price: "LONG" at the ask, "SHORT" at the bid. It returns the position with its id, or None when the account refuses it: lots that are not a positive number, or a free margin short of what it ties up.
  • ctx.positions is what is open, in the order it opened: id, label, direction, lots, entry_price, entry_time, stop_price, target_price, profit and pips at the current price, and commission.
  • ctx.modify_position(id, sl=..., tp=...) moves a stop or a target, as prices. None clears one, and one you leave out stays.
  • ctx.close_position(id) closes one now, lots=... part of it.
  • ctx.account.margin and ctx.account.free_margin are what the positions tie up and what is left, and ctx.bid and ctx.ask the price they trade at.

Each bar, every stop and target is checked against the bar first: a long's on the bid, a short's on the ask, at its level, or at the open when the bar opened past it. Then the strategy runs, then the account is marked. At zero equity every position closes and the run ends. Nothing is netted: a long beside a short is two positions. A hedging strategy does not use ctx.signal, ctx.order, ctx.close or ctx.position, and the run says so if it does. Rust robots and C# cBots trade on the same account, with the same rules.

What a pip is, per asset class

The engine converts a stop or a target into price by multiplying it by the instrument's pip size, and that size is not one number. It is the increment the instrument's own distances are quoted in.

Asset classctx.pip()A stop of 20 pips is
Forex0.0001, or 0.01 when the quote currency is JPY0.0020, about 0.18% of EURUSD at 1.0850
Metal0.01 on XAUUSD, 0.001 on XAGUSD0.20, about 0.01% of gold at 2,000
Index0.1, and 1.0 on US30, JP225 and HK502 index points, about 0.04% of US500 at 5,000
Crypto1.0 on BTCUSD and ETHUSD; ten price ticks on each archive coin (0.1 on SOLUSD, 0.01 on ADAUSD)20 USD, about 0.03% of BTCUSD at 60,000
Equity0.01, a cent0.20, about 0.2% of a share at 100
CFD0.01, and 0.001 on NGCUSD, GAS.CMD/USD and COPPER.CMD/USD0.20, about 0.27% of WTIUSD at 75

A custom symbol returns exactly the pip size you defined for it. A run that declares an asset class for an instrument nothing else can place gets that class's default row above. An instrument the engine cannot place at all stops the run before the first bar rather than being priced as a currency pair. See Asset Classes.

The consequence for a strategy is that a literal pip distance is not portable. The same sl_pips=20 is a fifth of a per cent on EURUSD and a hundredth of one on gold, so moving a pip sized strategy to another class runs a different strategy. Two forms survive the move:

  • sl_percent and tp_percent are a percentage of the last close, converted here with the same pip size, so one per cent means one per cent on every class. Before the first bar the distance is absent rather than zero. Giving both a percentage and a pip distance for the same side is a contradiction, and the pip value wins.
  • A distance derived from ctx.indicators.atr(period) and divided by ctx.pip() is already independent of the class, because the engine multiplies it straight back by the same pip size. The distance is then a multiple of the instrument's own volatility.

Parameters

__init__(self, params) reads parameters with params.get("key", default). Which keys the app offers is not read from your source: a saved strategy carries the parameter set of the built in it was created from, and those are the keys the fields beside a run and the optimizer drive. A key you read that is not in that set always takes the default written in params.get.

Example: moving average cross with percent stops

pythonclass PortableMaCross(Strategy):
    def __init__(self, params):
        self.fast = max(1, int(params.get("fast", 12)))
        self.slow = max(self.fast + 1, int(params.get("slow", 34)))
        self.sl_percent = float(params.get("slPercent", 1.0))
        self.tp_percent = float(params.get("tpPercent", 2.0))

    def on_bar(self, ctx):
        fast = ctx.indicators.sma(self.fast)
        slow = ctx.indicators.sma(self.slow)
        if fast is None or slow is None:
            return
        bars = ctx.bars(self.slow + 1)
        if len(bars) < self.slow + 1:
            return
        closes = [b.close for b in bars]
        fast_prev = sum(closes[-self.fast - 1:-1]) / self.fast
        slow_prev = sum(closes[-self.slow - 1:-1]) / self.slow
        if fast_prev <= slow_prev and fast > slow:
            ctx.signal("LONG", sl_percent=self.sl_percent, tp_percent=self.tp_percent)
        elif fast_prev >= slow_prev and fast < slow:
            ctx.signal("SHORT", sl_percent=self.sl_percent, tp_percent=self.tp_percent)

An indicator exposes its current value only, so the previous pair is computed from the bars. This one runs unchanged on a currency pair, an index, a coin and a share.

Example: EMA cross with ATR sized stops

pythonclass EmaAtrCross(Strategy):
    def __init__(self, params):
        self.fast = int(params.get("emaFast", 12))
        self.slow = int(params.get("emaSlow", 26))
        self.atr_period = int(params.get("atrPeriod", 14))
        self.atr_mult = float(params.get("atrMult", 2.0))
        self.rr = float(params.get("rr", 1.5))
        self.prev_fast = None
        self.prev_slow = None

    def on_bar(self, ctx):
        fast = ctx.indicators.ema(self.fast)
        slow = ctx.indicators.ema(self.slow)
        atr = ctx.indicators.atr(self.atr_period)
        if fast is None or slow is None or atr is None or atr <= 0:
            self.prev_fast, self.prev_slow = fast, slow
            return
        if self.prev_fast is not None and self.prev_slow is not None:
            sl_pips = (atr * self.atr_mult) / ctx.pip()
            tp_pips = sl_pips * self.rr
            if self.prev_fast <= self.prev_slow and fast > slow:
                ctx.signal("LONG", sl_pips=sl_pips, tp_pips=tp_pips)
            elif self.prev_fast >= self.prev_slow and fast < slow:
                ctx.signal("SHORT", sl_pips=sl_pips, tp_pips=tp_pips)
        self.prev_fast, self.prev_slow = fast, slow

ATR is a price distance, so dividing by ctx.pip() converts it to the units sl_pips expects. The stop is two ATRs whatever the instrument is.

Example: RSI mean reversion

pythonclass RsiReversion(Strategy):
    def __init__(self, params):
        self.period = int(params.get("rsiPeriod", 14))
        self.lo = float(params.get("oversold", 30))
        self.hi = float(params.get("overbought", 70))
        self.prev = None

    def on_bar(self, ctx):
        r = ctx.indicators.rsi(self.period)
        if r is None:
            self.prev = r
            return
        prev, self.prev = self.prev, r
        if prev is None:
            return
        if prev <= self.lo and r > self.lo:
            ctx.signal("LONG", sl_pips=30, tp_pips=45)
        elif prev >= self.hi and r < self.hi:
            ctx.signal("SHORT", sl_pips=30, tp_pips=45)

The 30 and the 45 here are forex sized. On US500 the same numbers are 3 and 4.5 index points, and on BTCUSD they are 30 and 45 US dollars.

Edge cases

  • Indicators return None until they have enough bars. Always guard before comparing.
  • Signalling every bar while a position is open is harmless: the engine reconciles intent against the open position. LONG on an open long does nothing, the opposite direction closes and reverses in one step, EXIT closes whatever is there. Detect crosses to avoid noise.
  • One position at a time on the standard engine.
  • strength is carried on the signal and ignored when sizing, so a value other than 1 changes nothing. A size is one of the arguments in The size of a position.
  • Entries and signal driven exits fill at the next bar's open. A protective stop or target is checked inside the bar, and a bar that opens beyond the level fills at the open, not at the level, so a gap costs what it cost.
  • If equity reaches zero the engine closes the position at that bar and stops the run. The result carries ruined and ruinedAt, so a short run is not mistaken for a strategy that stopped trading.

Taking a strategy to cTrader

A Python strategy can be exported as a cTrader cBot from the Build view: open the menu behind the three dots, choose Export, then cTrader cBot. The same panel also makes a MetaTrader 5 expert advisor from it, translated the same way. Pine and block strategies export to all three platforms too, printed from the engine's own strategy format; see Pine Script and Blocks. The translation is a parser, not a model: it either translates a construct exactly or it refuses the strategy and names the line that stopped it. Nothing is approximated and nothing is dropped, because a strategy that exports and then trades differently is worse than one that would not export.

The two vocabularies were lined up for this. on_bar becomes OnBar, ctx.symbol.pip_size becomes Symbol.PipSize, ctx.symbol.name becomes SymbolName, and ctx.indicators.sma(period) becomes Indicators.SimpleMovingAverage. The older names translate to the same C#: ctx.pip() is Symbol.PipSize, and ctx.sma(...) is the construction ctx.indicators.sma(...) produces.

What translates exactly: the bar window, the bar fields, the pip size, the symbol name, the simple moving average, RSI, ATR, module level helper functions, and the signal reconciliation. The quote and the book translate too: ctx.bid and ctx.ask become Symbol.Bid and Symbol.Ask, and ctx.depth(n) becomes MarketData.GetMarketDepth, best level first, with a size in units of the instrument and null where the broker publishes no book. A cBot reads them when the new bar's first quote arrives, where this engine read the last quote of the bar that had just closed, and the translation report says so. The sizes at the touch (ctx.bid_size, ctx.ask_size), a side's best_bid and best_ask, and the book's ts_ns are refused. The exponential moving average translates too, with the one difference below. What is refused: anything reading the economic calendar, whether ctx.news_data or an on_news method, a custom or plugin indicator, ctx.data, another instrument's bars, a signal strength other than 1, sl_percent and tp_percent, integer division and modulo, and loops. Only __init__ and the per bar hook are read from the class, so an on_stop method produces nothing in the cBot.

One difference is reported rather than refused. This engine seeds an EMA with the simple average of its first period and cTrader seeds it with the first price, so the two disagree for about one period after the cBot starts and agree from then on. The export says so in the translation report.

The MetaTrader 5 expert advisor carries the engine's own SMA, EMA, RSI and ATR rather than MetaTrader's iMA, iRSI and iATR, which compute other values: iATR is a simple average of the true range, this engine's ATR is Wilder's. It feeds them the closed bars it has when it starts, so its EMA, RSI and ATR begin from another bar than the backtest did and come together with it within a few hundred bars. A strategy that reads a library indicator with ctx.ind goes to both platforms through the engine's own strategy format, as Pine does, each indicator carried in the file.

A cBot is a different program on a different engine, with its own fills, spread and data. It will not reproduce the backtest, and the generated file says that in its first comment.

The workspace: files, a notebook and GitHub

Every strategy is a workspace. Build opens on Workspaces, a list of them with the languages of their files, the last run and whether they are on GitHub. Choose one to open it full screen: a tab for each file, the file that runs first and marked with a play sign, then the notebook's Markdown files. The Overview tab shows every file at once, with the language, the number of lines and, for a notebook file, its first heading, and the plugins the workspace uses and GitHub. Runs shows how it ran, its parameters and who may see it. Workspaces at the top left returns to the list.

The plus after the last tab adds a file in any language, a notebook file, an indicator from your library or a plugin from your library. The small arrow on the open tab renames it, makes it the file that runs, or deletes it, which asks first; a double click renames it too. The file that runs cannot be deleted. A code file waits for Save or Ctrl+S, and a run reads what was saved.

Your library: indicators and plugins

Indicators and plugins you write once belong to your account and reach every device you sign in on. Build's Indicators and Plugins tabs list them, and each has New, edit and delete. An indicator is a file that gives values (see below), in Python, C#, Pine, Rust or blocks, or a formula for the chart.

Indicator from your library, in a workspace's plus, copies the indicator into the workspace as a file of its own and notes where it came from. The copy is the workspace's: changing the indicator in your library changes no strategy and no stored run. When the library has a newer version, the file's tab shows it, and Update from library takes it, after asking. A formula is computed for every run on a device; Add here puts it on this device's chart indicators.

A workspace can carry formula indicators of its own, under Indicators of this workspace on its overview: New formula, or a formula from your library. Every run of the workspace computes them, wherever it runs, the strategy reads each with ctx.custom("name"), and the chart of the run draws them beside the device's. Like a file, a change waits for Save.

A plugin runs on the machine it is installed on. Plugin from your library notes that the workspace uses it and installs it on this device; on another device the workspace's overview shows it as not installed, with Install here. What a device has of its own stays usable under On this device, and Move to account copies it into your library.

Languages and the engine

A strategy's code files can be in any language a strategy is written in: Python (.py), C# (.cs), Pine Script (.pine), a blocks canvas (.blocks) and Rust (.rs), side by side in one workspace. One of them is the file that runs, marked in the tabs. Its own language's files are its helpers: a Python file imports the .py files, a C# file compiles with the .cs files, a Rust file reaches the .rs files as modules. A file in another language is a strategy of its own; the arrow on its tab makes it the file that runs.

Files in different languages work together through values: a file gives a number each bar under a name, and the file that runs reads it, as ctx.read("trend.pine", "up") in Python and Rust, Read("trend.pine", "up") in C#, workspace.read("trend.py", "up") in Pine and a Workspace value block on a canvas. A Pine file gives its top level declarations, a canvas its Output blocks, a Python file the dict its outputs(ctx) returns, a C# class deriving from Outputs what it Sets, a Rust file what it ctx.sets. A file that gives values places no orders and reads no other file.

The switch in the workspace's bar picks the engine the strategy runs on: the Python engine runs every language, the Rust core runs Pine, blocks and Rust many times faster. Files the chosen engine cannot run are greyed in the tabs with the reason, and a run on an engine that cannot run the strategy is refused with it, never moved to the other engine without a word.

The notebook

Every strategy in your library has a notebook: Markdown files you write beside the code, a page per subject, for example the idea, a journal and what a run showed. Each opens with a preview side by side. Dated entry adds a heading with today's date at the end of the file that is open. A notebook written before it had files keeps its text as notes.md.

The notebook saves itself a moment after you stop typing. It is never part of a run: it is not sent to the engine or the server with one, and writing in it does not make the strategy unsaved. After a backtest, Add to notebook in the head of the Evaluation writes the market, the period and the headline figures at the end of notes.md in the notebook of the strategy that ran, as a small table.

The notebook stays on your machine. It goes to the marketplace only when you tick it while publishing, as one page, and to GitHub only when you tick it while pushing. The docked assistant reads the start of it, because it often says what the code is meant to do.

Pushing to GitHub

Link GitHub once, on the workspace's overview or in Settings under Security. Your browser opens GitHub, you authorise the OrionQuant app, install it on your own GitHub account and choose which repositories it may write to. The workspace notices the installation by itself. No GitHub password or token is kept.

The first Push to GitHub, in the GitHub menu of the workspace's bar, asks where the strategy goes: a new repository, private unless you untick it, or one you already have, in a folder of its own. Every push after that goes to the same place and asks only for a message. The overview shows how many files changed since the last push.

A push writes one commit in your name, with no git on your machine:

  • every code file under its own name
  • every notebook file under notebook/, when you tick it
  • orionquant.json, what OrionQuant needs to open the repository again
  • README.md on the first push only, never overwritten after that

What is pushed is the strategy as saved. Save first, and the push button waits until you have.

A push never overwrites a change made on GitHub. When a file there differs from what OrionQuant pushed last, nothing is written, the files are named, and Push to a new branch keeps both versions for you to merge on GitHub. A protected branch is treated the same way. Only your own GitHub account can be linked for now, not an organisation.

Pulling from GitHub

Pull in the same GitHub menu brings in what changed on GitHub since the last push or pull. A strategy that was never pushed links to a repository first: Link a repository asks for the repository, the branch and the folder that hold it, and shows what is there before anything changes.

A pull lists what it would do, file by file: files changed on GitHub are taken, files deleted there and unchanged here are removed, files changed only here are kept for the next push. A file changed on both sides is shown with both versions, and you pick which one stays. Nothing is written until you press Pull, and the strategy is saved with the result. Save first: a pull waits until there is nothing unsaved.

OrionQuant