For the complete documentation index, see llms.txt. This page is also available as Markdown.

Tutorials

Ten complete strategies, from a first workflow to calendar-driven rebalancing.

Every JSON on this page was executed against a running OpenAlgo instance with a live broker connection in Analyzer mode. The log output shown is real output from that run, not illustration.

How to run these

  1. Open /flow, click Import

  2. Paste the JSON (or save it as a .json file and use the file picker)

  3. Click Save, then Run to execute once

  4. Read the execution log panel — every node reports what it did

Turn Analyzer mode on first. Orders will be simulated.


1. Quotes, maths, and variables

The smallest useful workflow: read a quote, compute the percentage move, log it. This teaches outputVariable and {{...}} interpolation.

{
  "name": "T1 quote and variables",
  "nodes": [
    { "id": "n1", "type": "start", "position": { "x": 0, "y": 0 },
      "data": { "scheduleType": "daily", "time": "09:20", "days": [0,1,2,3,4], "marketHoursOnly": true } },
    { "id": "q", "type": "getQuote", "position": { "x": 0, "y": 100 },
      "data": { "symbol": "RELIANCE", "exchange": "NSE", "outputVariable": "q" } },
    { "id": "m", "type": "mathExpression", "position": { "x": 0, "y": 200 },
      "data": { "expression": "({{q.data.ltp}} - {{q.data.prev_close}}) / {{q.data.prev_close}} * 100", "outputVariable": "chgPct" } },
    { "id": "l", "type": "log", "position": { "x": 0, "y": 300 },
      "data": { "message": "RELIANCE ltp={{q.data.ltp}} open={{q.data.open}} prevClose={{q.data.prev_close}} change={{chgPct}}%", "level": "info" } }
  ],
  "edges": [
    { "id": "e1", "source": "n1", "target": "q" },
    { "id": "e2", "source": "q", "target": "m" },
    { "id": "e3", "source": "m", "target": "l" }
  ]
}

Key point. mathExpression supports + - * / % ** and parentheses over interpolated values. It cannot do date arithmetic or call functions.


2. Indicator conditions and branching

Compute RSI, then take a different path depending on its value.

Verified output:

Key point. varCondition compares any two interpolated values. Unlike priceCondition (which always re-fetches a live quote field), it works on indicator outputs, previous-period levels, and literals. If an operand does not resolve to a number it refuses to evaluate and takes neither branch — a typo cannot silently route your else-path into a trade.


3. Crossovers

There is no crossover node — crossover needs two series. Build it from two indicator nodes and an andGate.

A golden cross means: fast is above slow now, and fast was at or below slow on the previous bar.

Verified output — a genuine EMA9/EMA21 cross on NIFTY daily:

For a death cross, flip both operators: < on latest, >= on previous.

To detect a cross on an earlier bar, add offsetBars to both indicators and compare {{fast.at_offset.value}} against {{slow.at_offset.value}}.


4. Multi-timeframe filter

Trade only when the daily trend and the 15-minute momentum agree.

Verified output:

Key point. Two indicator nodes on the same symbol at different intervals are two distinct fetches — the cache only collapses identical requests. That is correct and unavoidable.


5. Previous-day breakout with a gap filter

The classic PDH breakout, with the refinement that a gap-up open should not trade until price has come back to test the level.

Flow has no memory between runs, so "price returned to PDH earlier today" cannot be a stored flag. Instead read today's session low from the quote: if the day's low is at or below PDH, price has already visited that level today. That makes the filter entirely stateless.

Verified output — the filter correctly declined a gap-up that never retested:

For the PDL short side, mirror it: {{q.data.ltp}} < {{pd.pdl}} and {{q.data.high}} >= {{pd.pdl}}.

"One trade per breakout" is enforced by positionCheck with not_exists — it asks the broker, so it survives restarts, unlike a counter variable.

To actually trade it, replace the go log node with the options entry from Tutorial 7.


6. Historical lookback

Previous day, previous week, a bar five sessions back, and an indicator value five bars back — all in one workflow.

Key point. offsetBars: 5 on a daily chart is five trading bars, which spans seven calendar days across a weekend. Supertrend legitimately holds a flat level through a sustained trend, so identical values at different offsets are often correct, not a bug — check out1 (direction) to confirm.


7. Risk-guarded ATM options entry

Two guards — available funds and no existing position — before buying a current-week ATM call.

Verified output (Analyzer mode):

Key points.

  • optionsOrder resolves the expiry and strike for you — you never hand-write NIFTY04AUG2624250CE.

  • quantity here is in lots. placeOrder, smartOrder, splitOrder, and basketOrder take quantity in shares. This asymmetry is the single most common sizing mistake.

  • expiryType accepts current_week, next_week, current_month, next_month.

  • offset accepts ATM, ITM1ITM5, OTM1OTM10.


8. Reacting to a fill

orderUpdateTrigger fires the moment a matching order changes status — no polling loop.

The event payload is exposed as {{webhook.*}}: orderid, symbol, exchange, order_status, filled_quantity, average_price, rejection_reason.

Key points.

  • Set at least one of Order ID or Symbol. An unfiltered watch would fire on every order in the account and is rejected.

  • Order ID must be a literal broker order id — a trigger has no upstream node, so {{ce.orderid}} cannot be resolved and is rejected with a 400. Filter by symbol instead.

  • status accepts any, open, trigger pending, complete, rejected, cancelled.

  • trigger: "once" stops watching after the first match; "every_time" keeps watching.

  • Watches survive a restart — they are restored from active workflows on boot.


9. Exit on this strategy's own P&L

The account's position book nets everything and carries no strategy label, so "am I up 5000 on this strategy" cannot be answered from positionBook when another strategy holds the same contract. strategyPnl answers it.

Verified with one open leg of 75 NIFTY CE bought at 100 and marked at 170:

Key points.

  • Leave strategy blank. It defaults to the workflow's own name, which is the same tag this workflow's order nodes apply, so entry and exit agree with no configuration.

  • The open_quantity != 0 guard first. Without it the workflow re-fires closePositions every minute after the position is already flat, because realized P&L stays above the target for the rest of the session.

  • total is realized + unrealized. Use today_realized for an intraday-only target that ignores P&L carried in from previous sessions.

  • Swap >= for <= and a negative number to get a stop-loss instead.

  • closePositions is account-wide, not strategy-scoped. If several strategies hold positions at once, close the specific leg with a placeOrder or smartOrder (positionSize: 0) instead.

Before relying on this. The strategy book is built from orders placed through OpenAlgo with a strategy tag — Flow nodes and /api/v1/ calls that carry strategy. A position you opened by hand in the broker terminal is invisible to it, so its P&L is not counted. Check unpriced_legs too: it counts open legs with no live price, which are excluded from unrealized, so a non-zero value means total is understated.


10. Run only on the first trading day of the month

A monthly rebalance must fire once, on the first day the exchange actually trades. {{day}} == 1 gets this wrong whenever the 1st is a weekend or a holiday. The calendar node answers it directly.

Key points.

  • The schedule runs every weekday; the calendar node decides whether today is the day. That is deliberate — a monthly schedule could not know which date the exchange actually opens on.

  • Swap is_new_month for is_new_week, is_new_quarter or is_new_year. Use is_last_day_of_month for a month-end square-off instead.

  • Flow keeps no state between runs, so this cannot work by remembering the last run. It does not need to: "a new month started" is the same statement as "today is the first trading day of this month", and the exchange calendar answers that on its own.

  • is_trading_holiday is distinct from is_weekend, so you can log why a day was skipped.

  • Blank date uses the current trading session date, which differs from the calendar date between midnight and the 03:00 IST rollover.

Real 2026 dates this handles correctly, and the naive tests do not:

Date

Day

is_new_month

is_new_week

1 Aug

Saturday

false

false

3 Aug

Monday

true

true

26 Jan (Republic Day)

Monday

false

false

27 Jan

Tuesday

false

true


Where to go next

Last updated