For Each
kaya-for-eachCorefor_each0 crVariants
One module per value type. They behave identically; pick the one matching the type you are wiring.
| Name | Type |
|---|---|
| For Each Boolean | boolean |
| For Each Boolean Array | booleanarray |
| For Each Boolean Datapoint | booleandatapoint |
| For Each Boolean Matrix | booleanmatrix |
| For Each Boolean Series | booleanseries |
| For Each Crypto Ticker | cryptoticker |
| For Each Double | double |
| For Each Double Array | doublearray |
| For Each Double Datapoint | doubledatapoint |
| For Each Double Matrix | doublematrix |
| For Each Double Series | doubleseries |
| For Each Float | float |
| For Each Float Array | floatarray |
| For Each Float Datapoint | floatdatapoint |
| For Each Float Matrix | floatmatrix |
| For Each Float Series | floatseries |
| For Each Integer | integer |
| For Each Integer Array | integerarray |
| For Each Integer Datapoint | integerdatapoint |
| For Each Integer Matrix | integermatrix |
| For Each Integer Series | integerseries |
| For Each Stock Ticker | stockticker |
| For Each String | string |
| For Each String Array | stringarray |
| For Each String Datapoint | stringdatapoint |
| For Each String Matrix | stringmatrix |
| For Each String Series | stringseries |
Every strategy contains a number you guessed. A moving-average window, a stop-loss percentage, a lookback for a z-score. You picked 20 because 20 felt reasonable, and never went back.
For Each is the module that goes back for you. Instead of one window you declare a whole set, and Systemic runs your strategy for every one, together, on the same data. A single answer becomes a landscape of answers — and the shape of that landscape tells you more than any point on it.
The family has many typed variants: integers, floats, tickers, and every kind of series or array you might iterate over. They behave identically, differing only in what kind of value they hand you.
What a For Each Does
A For Each defines an iteration set and hands out its members one at a time. Everything downstream is duplicated once per member — the branch fans out.
flowchart LR
FE["For Each Integer<br/>From 10, To 50, Step 10"]
subgraph FAN["One branch, executed 5 times"]
direction TB
E1["element = 10"] --> S1["Strategy branch"]
E2["element = 20"] --> S2["Strategy branch"]
E3["element = 30"] --> S3["Strategy branch"]
E4["element = 40"] --> S4["Strategy branch"]
E5["element = 50"] --> S5["Strategy branch"]
end
COL["Filter or Collapse<br/>consumes the dimension"]
OUT["A single result"]
FE --> E1 & E2 & E3 & E4 & E5
S1 & S2 & S3 & S4 & S5 --> COL --> OUT
style FE fill:#3498db,color:#fff
style COL fill:#27ae60,color:#fff
style OUT fill:#d5f5e3,stroke:#27ae60
style FAN fill:#ecf0f1,stroke:#bdc3c7
You draw the branch once. The loop is what multiplies it.
It Is Not a Loop That Runs Five Times
A sweep is not a repeat button. Before anything starts, your branch is duplicated once per value — five values, five copies, identical except for the number substituted into each — and all five run together as one strategy over the same data.
- There is no "first" or "last" iteration. No copy sees another's result or passes a value to the next. If your idea needs one run's answer to feed the next, a sweep is the wrong tool.
- The cost is width, not wait. Five copies of a twenty-node branch is a hundred nodes of work at once — exactly what the limits below count.
You Cannot Put a For Each Inside a For Each
A For Each may not be placed anywhere downstream of another For Each. Such a strategy is rejected before it runs, with a validation error stating that nested For Each loops are not supported.
To sweep two parameters at once you do not nest. You place two independent For Each modules on separate branches and let those branches meet downstream — the only way to combine loops.
Defining the Iteration Set
Four ways, depending on the type you are iterating over.
1. Numeric ranges — Integer, Float, Double
You give From, To, Step, and a Scale — linear (the default) or logarithmic.
Through the API these are the static inputs foreach.range.from, foreach.range.to, foreach.range.step and foreach.range.scale (linear or log).
Linear. Values climb by Step until they would pass To:
Requires Step greater than zero and From no larger than To.
Logarithmic. Values are multiplied by Step each time:
Requires From greater than zero and Step greater than one — a smaller multiplier would never advance.
Worked Example: From 10, To 50, Step 5, Linear
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|---|
| element | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 |
To is a ceiling, not a destination. Change To to 52 and you get the same nine values — 55 would overshoot, so the range stops at 50.
Linear or Logarithmic?
| Setting | Scale | Values | Count |
|---|---|---|---|
| From 10, To 100, Step 10 | Linear | 10, 20, 30, … , 100 | 10 |
| From 2, To 64, Step 2 | Logarithmic | 2, 4, 8, 16, 32, 64 | 6 |
| From 0.001, To 1, Step 10 | Logarithmic | 0.001, 0.01, 0.1, 1 | 4 |
Use linear when equal spacing matters — windows in days, holding periods in bars. Use logarithmic when the quantity spans orders of magnitude: sweeping a stop-loss from 0.001 to 1 linearly would need a thousand steps, logarithmically four.
2. Boolean
A checklist: tick true, false, or both. At most two elements — the cleanest possible A/B test.
Through the API this is the static input foreach.bool.selection: true, false or true,false.
3. Enum — Crypto Ticker, Stock Ticker
Choose a subset of the available symbols, or take all of them. The elements come out sorted alphabetically by canonical symbol, so the ordering is stable whatever order you clicked them in. This is the variant that turns a strategy into a screener.
Through the API this is the static input foreach.enum.subset: ALL, or a JSON array of canonical symbols such as ["kc:BTC-USDT","kc:ETH-USDT"]. Any other key is ignored and the loop runs over every symbol.
One loop can drive every feed of its market at once. The loop iterates the market's symbols, BTC-USDT or NVDA, and every feed wired to it reads that symbol under its own provider: a crypto loop feeds the price candles and the perpetuals sources for open interest, funding rate, long/short ratio and liquidations; a stock loop feeds the price bars, the SEC filing sources (Fundamentals, Balance Sheet, Income and Cash Flow) and Insider Transactions. Not every provider serves every symbol, so the loop iterates only the symbols every linked source can serve: a symbol a linked feed lacks is struck out and locked in the loop, and the count badge shows what will actually run. Your selection is never edited for you; unlink the feed and the symbol comes back. A selection with nothing left for a linked feed is a validation error naming that feed. A source that publishes each symbol on a fixed cadence takes the cadence all its symbols share; the editor fills it in when you connect the loop.
4. Variadic — Array, Matrix, Series, Datapoint, String
Instead of settings, these give you input slots. Each wired slot becomes one iteration, in slot order, and at least one must be wired. This is how you sweep over things that are not numbers — three price series, four candidate baskets, a handful of labels.
One Loop, One Dimension
Each For Each contributes exactly one dimension to everything downstream of it.
Two Independent Loops Multiply
Two independent For Each modules — each on its own branch, neither downstream of the other — whose branches meet at a common node produce the Cartesian product of their sets. A window loop over and a ticker loop over AAPL, MSFT meet as six combinations: 10·AAPL, 20·AAPL, 30·AAPL, 10·MSFT, 20·MSFT, 30·MSFT.
For independent loops of sizes converging on one node:
The Same Loop Twice Does Not
If a single For Each reaches a node by two paths — its element feeding both a moving average and a threshold further along — that is still one dimension. The loop is zipped with itself, not squared: element 20 on the left meets element 20 on the right, never element 30. The two paths must agree on their count, or you get a validation error rather than a silently wrong grid.
The Cap: 20 Combinations
Twenty is the number to remember, and it binds in two places:
- No single loop may have more than 20 elements. A range, enum subset, or slot count resolving to 21 or more is rejected on its own.
- No node may see more than 20 combinations. Loops of 5 and 4 are fine, and so is their meeting point at 20. Make them 5 and 5 and the meeting point is rejected at 25, though neither loop is oversized.
Both errors are raised before the strategy runs — you find out while building, not after a long wait.
| Independent loops | Sizes | Combinations | Status |
|---|---|---|---|
| 1 | 20 | 20 | At the ceiling |
| 1 | 21 | 21 | Rejected — the loop itself is too large |
| 2 | 5 × 4 | 20 | At the ceiling |
| 2 | 5 × 5 | 25 | Rejected — each loop is legal, the meeting point is not |
| 3 | 3 × 3 × 2 | 18 | Comfortable |
| 3 | 3 × 3 × 3 | 27 | Rejected |
Three parameters at five values each is 125 combinations. Four is 625. Five is 3,125. This is combinatorial explosion, and it is why the ceiling exists.
A second, wider ceiling caps total duplicated work — every combination is another full copy of the swept branch:
Twenty combinations across a twenty-five-node swept region sits exactly at that limit. Sweep a smaller region and you can afford more combinations; sweep your whole strategy and you cannot.
A Real Sweep: Moving-Average Windows on Bitcoin
A For Each Integer with From 20, To 120, Step 20 gives six values — — namely 20, 40, 60, 80, 100 and 120.
The test. Daily BTC/USD closes, 2021-01-01 through 2025-12-31: 1,826 closes, 1,825 decision days. The rule is deliberately crude — at each close, if price sits above the simple moving average of the last closes, hold Bitcoin through the next day, else hold cash earning nothing. No fees, no slippage, no interest. Each window is primed on closes preceding the start date, so all six see the same 1,825 decisions. Buy-and-hold returned +198.63%.
xychart-beta
title "BTC/USD 2021-01-01 to 2025-12-31: total return by window"
x-axis "Moving-average window (days)" [20, 40, 60, 80, 100, 120]
y-axis "Total return (%)" 0 --> 650
bar [64.8, 573.9, 339.2, 260.0, 373.6, 611.6]
line [198.63, 198.63, 198.63, 198.63, 198.63, 198.63]
Bars: total return per window. Flat line: buy-and-hold.
| Window | Days in market | Total return |
|---|---|---|
| 20 | 927 / 1825 (50.8%) | +64.8% |
| 40 | 940 / 1825 (51.5%) | +573.9% |
| 60 | 979 / 1825 (53.6%) | +339.2% |
| 80 | 1009 / 1825 (55.3%) | +260.0% |
| 100 | 1028 / 1825 (56.3%) | +373.6% |
| 120 | 1054 / 1825 (57.8%) | +611.6% |
The spread runs from +64.8% to +611.6%, nearly a factor of ten, from changing one setting on one module. Worse, it is not orderly: 40 is spectacular, 60 and 80 ordinary, 100 recovers, 120 best of all. Time in the market barely moves, 50.8% to 57.8% — the differences are not about exposure but about which particular days each window sat out. Build this with a window of 40, stop there, and you would believe you had found something.
The Overfitting Trap
The obvious move now is to keep the best number. Window 120. +611.6%. Done.
Do not do that. Split the same test in two: choose on 2021–2023, then check that choice on 2024–2025, which it never saw.
| Window | 2021–2023 (chosen on this) | 2024–2025 (judged on this) |
|---|---|---|
| 20 | +18.5% | +39.1% |
| 40 | +215.4% | +104.4% |
| 60 | +153.9% | +65.5% |
| 80 | +113.1% | +61.6% |
| 100 | +160.4% | +74.1% |
| 120 | +249.2% | +95.0% |
| Buy-and-hold | +44.1% | +98.3% |
The in-sample winner is window 120. Out of sample it came second of six and lost to simply holding Bitcoin — +95.0% against +98.3%. The number that looked like a discovery was, in the period that mattered, worth less than doing nothing.
This is arithmetic, not bad luck. Evaluate variants and keep the maximum, and that maximum is inflated by the noise in your sample — more so the larger gets. As Bailey, Borwein, López de Prado and Zhu put it in Pseudo-Mathematics and Financial Charlatanism: with enough trials, a backtest with a superb in-sample record and zero genuine edge is not merely possible but expected.
Which brings the cap of 20 into focus. It is not a performance limit dressed as a rule. It is a limit on how far you can fool yourself in one pass.
How to Read a Sweep Honestly
- Read the shape, not the peak. A smooth ridge, where neighbouring values behave alike, suggests something real. A peak next to a trough is the signature of noise — and the six windows above are jagged.
- Compare against doing nothing. Over the full span five of six windows beat buy-and-hold; over the out-of-sample half, only one did.
- Judge on data you did not choose on — reserve a period, or roll the fit forward, as in walk-forward analysis.
- Count everything you tried, including the sweeps you abandoned. That total is the honest , and it governs how impressed you should be. This is data dredging, and it is easy to do by accident.
A sweep is not a search for the winning number. It is a robustness test — asking whether your idea survives being nudged.
Closing the Loop
A fanned-out branch cannot stay fanned out. Every dimension a For Each opened must be closed by a Filter, which keeps the combinations satisfying a condition, or a Collapse, which reduces every combination to one value — a best, an average, a ranked pick. Either consumes the dimension and returns you to a single branch.
Two loops open two dimensions, and both must be consumed; leave one open and you get a validation error naming it. See the Filter and Collapse pages for how to choose between them.
Missing Data
A For Each never invents values and never skips them. The iteration set is fixed from your settings alone, before any market data is touched — a range of six values is six copies whether the data behind them is pristine or full of holes.
Missing data inside a copy stays inside that copy. If one combination produces gaps — a moving average not yet warmed up, an asset that did not trade that day — the others are untouched. Nothing is copied sideways between combinations, and a gap in one is never filled from another.
The variadic variants are the place to watch. Wire an empty or missing series into a slot and the slot is still an iteration; it simply yields nothing useful downstream. The count comes from how many slots you wired, not from how much data arrived in them.
Edge Cases
| Situation | What happens |
|---|---|
| Linear range with Step , or From > To | Validation error |
| Logarithmic range with From , or Step | Validation error — such a sequence cannot advance |
| From equals To (linear) | One element. A perfectly valid loop of size 1 |
| To not landing on a step boundary | The range stops at the last value To |
| Boolean with nothing ticked, or enum with no symbols chosen | Validation error — an empty set has nothing to emit |
| Variadic with no slot wired | Validation error — at least one slot is required |
| A single loop resolving to more than 20 elements | Validation error naming the loop and its size |
| More than 20 combinations at a node | Validation error, even when every loop is within its own limit |
| A For Each downstream of another For Each | Validation error — nested For Each loops are not supported |
| Combinations × enclosed nodes above 500 | Validation error — shrink the swept region or the set |
| A dimension never closed | Validation error naming the unconsumed loop |
Every one is caught while you are building. A For Each will not let you start a run it cannot finish.
Further Reading
- Pseudo-Mathematics and Financial Charlatanism — Bailey, Borwein, López de Prado and Zhu, Notices of the AMS, May 2014. Why the best backtest in a large sweep is usually the luckiest one.
- The Probability of Backtest Overfitting — the same authors' framework for putting a number on that risk.
- "…and the Cross-Section of Expected Returns" — Harvey, Liu and Zhu, NBER Working Paper 20592. On raising the bar for significance once you count how many ideas were tested.
- Overfitting and data dredging — why a model that fits the past perfectly usually fits the future badly.
- Walk-forward optimization — choosing parameters on one stretch of data and judging them on another.
- Cartesian product and combinatorial explosion — the arithmetic of combining loops, and why it runs away.