Event vs State Signals: Why Cross Trades Last One Bar

A crossover is an event, a trend is a state. Mixing them with signal-flip exits makes 1-bar trades or heavy churn. Real backtest numbers, and three fixes.

Trigr Research6 min read
On this page
  1. What is the difference between an event and a state?
  2. Why does a crossover trade close after one bar?
  3. What goes wrong if you switch to a state?
  4. How do you wire an event strategy correctly?
  5. What does delivery rising_edge add?
  6. Why does the timeframe of an event matter?
  7. What should you check before trusting a backtest?

TL;DR: A crossover is an event: true on one bar. "Fast average above slow average" is a state: true for as long as it holds. Signal-flip exits assume your entry signal is a state, so they close a trade as soon as the signal ends. Use an event with those exits and every trade lasts one bar; use a noisy state and the strategy churns. In one SOLUSDT 1h test, the same idea produced 127 one-bar trades and a loss, 3,874 trades and a 60.7% loss, or a 6.2% gain, depending only on how entries and exits were wired. The fix is to decide what opens a trade and what closes it, separately.

What is the difference between an event and a state?

Most trading ideas can be written both ways:

Idea As an event (true on one bar) As a state (true while it holds)
Trend change crosses_above(ema(close, 12), ema(close, 26)) ema(close, 12) > ema(close, 26)
Breakout crosses_above(close, lag(highest(high, 20), 1)) close > lag(highest(high, 20), 1)
Oversold crosses_below(rsi(close, 14), 30) rsi(close, 14) < 30

On SOLUSDT 1h data from September 2020 to October 2026, the EMA crossover event was true on 900 bars for each side, about 1.7% of the time. The equivalent state was true on roughly half of all bars. Neither is better. They answer different questions: "when did it change?" versus "is it true now?"

The trouble starts when the entry and the exit assume different answers.

Why does a crossover trade close after one bar?

Many strategy engines, Trigr included, offer an exit that follows the signal: hold while the signal agrees with your position, close when it turns flat or opposite. In Trigr this is exitOnFlip, and it is on by default because it is the natural exit for state signals.

With an event trigger, the signal is flat on the bar after the event. The exit sees "signal no longer long" and closes the position at the next open. We ran exactly this setup: a formula trigger on a 12/26 EMA cross with a 4h trend condition, a BTC-direction filter and an ADX filter, 10% sizing, 5 bps slippage and real funding. Every one of the 127 trades lasted exactly one hour. The result was a small loss of 1.5%, made almost entirely of fees and slippage, and the exit reason on every trade read "flip", which hides the cause.

Nothing was wrong with the idea. The wiring guaranteed it could never be held.

Trigr now flags this: a backtest whose trigger uses event logic with exitOnFlip on returns an expression_event_exit_on_flip warning and a "Strategy configuration" review line.

What goes wrong if you switch to a state?

The obvious fix is to replace the crossover with the state ema(close, 12) > ema(close, 26). Trades now last as long as the trend does, but a second mechanism appears: filters.

With exitOnFlip on, Trigr also treats a filter that stops passing as the signal going flat, unless you set filtersCloseTrades to false. Our strategy had a filter requiring adx(14) > 20 and volume > rolling_mean(volume, 20). Volume crosses its own 20-bar average almost every other bar, so the filter flickered and each flicker closed the trade. The state version made 3,874 trades and lost 60.7% after costs. Slippage and funding alone cost 25.6% of starting equity, so the gross loss was 35.1%.

High turnover is one of the cheapest ways to destroy a backtest, which the impact of trading fees on perps covers in detail. Trigr's results now carry a filters_close_trades warning in this situation, and a "High turnover" review line when a strategy trades about every other bar.

How do you wire an event strategy correctly?

Decide what opens a trade and what closes it, separately:

  1. Open on the event. Keep the crossover as the trigger.
  2. Close on explicit exits. Add exitLong and exitShort formulas on the RISK node, plus a stop. A natural exit for a crossover entry is the opposite state: ema(close, 12) < ema(close, 26).
  3. Turn signal-flip exits off. Set exitOnFlip to false. Trigr accepts this only when every traded side has an exit formula, a stop, a target or another exit. Leave onOpposite unset (or ignore): with flip exits off, an opposite signal never closes the position, so reverse is refused. If you want to reverse on the opposite cross, make each side's exit the other side's entry: exitLong = the short entry condition, exitShort = the long one. The exit and the new entry fill at the same next-bar open, and the trade still holds through flat bars.
  4. Let filters gate entries only. Set filtersCloseTrades to false.
TRIGGER long : crosses_above(ema(close, 12), ema(close, 26)) and close@4h > ema(close@4h, 50)
RISK         : sl 3, exitOnFlip false, filtersCloseTrades false
RISK exitLong: ema(close, 12) < ema(close, 26) or rsi(close, 14) > 75

Same entries, same filters, same costs. The result went from 127 one-bar trades and a 1.5% loss to 127 trades held until the trend reversed or RSI overheated, a 6.2% net gain and a profit factor of 1.33. That is a modest result on 10% sizing over six years, and it proves nothing about the future. What it does show is that the first version never tested the idea at all.

What does delivery rising_edge add?

Sometimes you want to keep a state formula but enter only when it first becomes true. Trigr's expression TRIGGER has a delivery setting for this:

  • level (the default): the entry signal repeats on every bar the formula is true.
  • rising_edge: the trigger emits one pulse on the bar where the whole formula changes from false to true.

On the state ema(close, 12) > ema(close, 26), rising_edge reproduces crosses_above(ema(close, 12), ema(close, 26)) exactly: same 900 entry bars, same 1,800 trades, same result. That makes it a convenient way to turn any composite condition into an event without rewriting it with cross functions. The Expression node overview lists where delivery applies; filters, regimes and exits cannot set it.

Why does the timeframe of an event matter?

There is one more trap. A trigger node can run on a coarser timeframe than the strategy, for example a 4h crossover on a 1h strategy. The 4h value only changes when a 4h bar closes, and it is carried forward across the four 1h bars until the next one. With the default level delivery, a 4h crossover therefore stays true for four consecutive 1h bars.

If an exit closes the trade during those four bars, the entry condition is still true and the strategy can re-enter on the same, now stale, crossover. In our test, the 4h crossover was true on 900 hourly bars per side for about 225 actual crosses. With level delivery the strategy made 227 trades; with rising_edge it made 128. The 99 extra trades were re-entries on old events. If an event trigger runs on a coarser timeframe than the strategy, set delivery to rising_edge. Multi-timeframe strategies without look-ahead bias covers how higher-timeframe values are carried forward.

What should you check before trusting a backtest?

A short checklist catches nearly all of this:

  • Look at holding times in the trade log. If most trades last exactly one bar, an event is feeding a signal-flip exit.
  • Compare trade count with the number of signals. Far more trades than entry events means filters or exits are churning.
  • Read the exit reasons. A wall of one-bar "flip" exits on an event strategy is the one-bar trap. A wall of short "flip" exits on a state strategy usually means filters are churning; the log labels both "flip", so rerun with filtersCloseTrades set to false to confirm.
  • Compare gross and net. If slippage and funding are a large share of the gross move, the strategy is trading too often to survive real execution.
  • Read the warnings. Trigr now flags event triggers with flip exits, filters that close trades, and high turnover.

The underlying idea is old: a crossover is an edge-triggered signal and a level is a level-triggered one, a distinction that comes from digital electronics. Pine Script users meet the same issue with ta.crossover entries. The strategy builder docs summarize how exitOnFlip, filtersCloseTrades and exit formulas are set.

Backtests are not guarantees, and perps are leveraged instruments that can lose more than expected.

Frequently asked questions

What is the difference between an event signal and a state signal?

An event is true on the single bar where something happens, like a moving-average crossover. A state stays true for as long as a condition holds, like the fast average being above the slow one. The same idea can be written either way, and the choice changes how a strategy enters and exits.

Why does my crossover strategy close every trade after one bar?

Because with exit-on-signal-flip enabled, a trade closes when the entry signal stops being true. A crossover is true for one bar, so the signal ends one bar after entry and the position closes. Turn exitOnFlip off and give the trade an explicit exit formula, stop or target.

What does delivery rising_edge do in Trigr?

It turns any TRIGGER formula into an event: the trigger emits one pulse on the bar where the whole formula changes from false to true, instead of repeating on every bar it stays true. On a state formula such as ema12 above ema26, it reproduces a crossover exactly.

Do filters close trades too?

By default, yes. With exitOnFlip on and filtersCloseTrades left at its default of true, a filter that stops passing closes the open position. Set filtersCloseTrades to false if your filters should only gate new entries.

Put the idea to an honest test.

Describe a strategy in plain English or from your own AI assistant, backtest it on point-in-time data, and forward-test it on paper before any real money is involved.