How the broker decides when and at what price orders fill, how a bar that could have filled two levels is settled, and how the run reports its own precision. The rules are the kScript (legacy) engine's: the host runs the engine's broker, one bar ahead of your file.
The execution model
Orders are processed at each new confirmed bar in a fixed sequence, before your file's state() runs on that bar:
- Protective exits of the open position (stop, limit, trail) are evaluated against the bar's range under the fill model.
- Pending signal exits from
closeandcloseAll, in insertion order. - Pending entries, in insertion order: market orders at the open, resting limit and stop orders if touched.
- On perps, the phase-end liquidation check against the final position.
- Your file runs at the bar's close; the orders it places join the queue for the next bar.
Fill prices follow bar mechanics:
- Market fills execute at the next bar's open, slippage-adjusted.
- Limit fills execute when the bar trades through the level (long entry:
low <= limit), at the better of the open and the limit, with no slippage: a limit price is a bound by definition. - Stop fills mirror the touch rule and are slippage-adjusted, because stops cross the market.
- Trailing stops activate when favorable excursion reaches the trail points, then ratchet with new extremes and fill under stop rules with
exitReason: "trail".
Equity is recorded on every bar (cash plus the open position marked at that bar's close, fees already paid), so drawdown and exposure account for flat periods too.
one confirmed bar
---------------------------------------------------------------->
[funding settles] [protective exits] [signal exits] [entries fill]
perps only stop / limit / close() / market at open,
trail vs the closeAll() resting limit /
bar's range stop if touched
| |
v v
.................. phase-end liquidation check ..................
|
v
[state() then finalize() run at bar close] -> orders placed here queue for the NEXT barTwo consequences worth internalizing: your file always sees the bar's completed fills, and nothing your file places can fill until the following bar. The first is why a flat guard works without a flag of its own:
// The flat guard: an entry only while flat, read from the position AFTER the bar's fills, so the bar an exit filled on can re-arm the entry.
import { input, line, lower, none, ohlcv, output, overlay } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_position, out_sma } from "./gen/outputs";
import { strategy } from "./gen/strategy";
import { Sma } from "./sdk/ta";
strategy({ qtyType: "fixed", qtyValue: 1 });
input("close", ohlcv.close);
output("sma", line, overlay, { description: "10-period SMA of close" });
output("position", none, lower, { description: "Signed position after the bar's fills" });
const sma = new Sma(10);
let mean: f64 = NaN;
let close: f64 = NaN;
let held: f64 = 0;
export function init(): void {}
export function state(): i32 {
close = in_close();
mean = sma.update(close);
held = strategy.positionSize();
return isNaN(mean) ? 0 : 1;
}
export function finalize(): void {
if (held == 0 && close > mean) strategy.long("L").send();
if (held > 0 && close < mean) strategy.close("L");
out_sma(mean);
out_position(held);
emitRow();
}
export function reset(): void {
sma.reset();
mean = NaN;
close = NaN;
held = 0;
}strategy.positionSize() inside state() reads the position after the bar's fills. On the bar the close("L") order filled, held is already 0, so the entry guard can re-arm on that very bar; keep entry zones disjoint from your exit prices unless that is what you want. The position output shows the same number the host publishes as strategy.position.
The newest bar
On a full run (a backtest, a chart's first render) every bar is confirmed, the newest included: an order placed on the last bar rests pending, exactly as the engine's full run treats it. On a live chart the newest bar is the forming bar: the broker marks equity at its close but fills nothing on it, and an order placed on it is rejected and counted, exactly as the engine treats a live update's last bar. Live, every tick re-evaluates the forming bar on a clone of the committed state; when the bar closes, the closed row is folded into the committed broker once (fills for the orders queued before it, then the file's own orders) and the next forming bar starts on a fresh clone. A live session therefore equals a full run from bar 0, including the one-time shift of pending-order and stat values between the first render and the first tick.
Intrabar ordering and fill models
A single bar tells you the range it traded, not the path it took. When both a protective stop and a profit limit sit inside one bar's range, the bar alone cannot say which traded first. Decisions certain at bar level never involve an assumption (a leg already marketable at the open fills at the open); the rest settle by the declared fillModel:
"pessimistic"(default): the stop is assumed to have filled first. The decision is counted inambiguousFillCountand the trade is flaggedambiguousFill."pathHeuristic": the open moves toward the nearer of high and low first. Natural path resolutions are not counted; only exact distance ties fall back to the pessimistic rule and count.
Both models are fully deterministic. This release attaches no finer-interval bars, so every contested fill settles by the model: the run details report bar resolution, and fineResolvedCount stays 0. Uncontested fills are identical either way.
Choosing an interval to trust
Brackets arm on the bar after the entry fills. On a 1-hour chart that protection gap is an hour of price movement; on a 1-minute chart it is a minute. A strategy whose stops are tight relative to its interval will genuinely behave differently at finer intervals, because it is seeing different information. Backtest at the interval you intend to run on, keep stops and targets wide relative to the interval's typical bar range or move to a finer chart, and treat ambiguousFillCount as part of the result.