Skip to content
Open RaamTrade

Persistent State

Persistent state is a value that survives from one bar to the next. There are two ways to get one, and the difference matters:

Scalars (int, float, bool, str) Collections (list, dict)
Declared in the class body Optional — count = 0 Required — vals = [0.0]*5
Created by first assignment in execute() Yes — self.count = 1 No
Rebindable in execute() Yes — self.count = 7 No — mutate by index or key

Scalars

Assign to self.<name> from inside execute() and the transpiler treats the first assignment as the declaration:

class UpCount(Strategy):
trade = Symbol()
sizing = FixedCapital()
def execute(self):
# Track consecutive up bars — reads as 0 the first time
if self.trade.close > self.trade.open:
self.up_count = self.up_count + 1
else:
self.up_count = 0
if self.up_count >= 3 and self.trade.is_flat:
self.buy(self.trade, reason=f"{self.up_count} up bars")

Declaring the slot in the class body instead is equivalent, and it is the only way to give it a non-zero starting value:

class Seeded(Strategy):
trade = Symbol()
sizing = FixedCapital()
ath = 0.0
label = "flat"
def execute(self):
if self.trade.close > self.ath:
self.ath = self.trade.close
if self.trade.is_flat and self.trade.close < self.ath * 0.95:
self.buy(self.trade, reason=self.label)

Scalar rules

  • First assignment fixes the type. Assigning an int and later a float to the same slot raises RC2_STATE_TYPE_REASSIGN. The other way round is fine: a whole number into a float slot is stored as a float (self.level = 0 after level = 0.0).
  • Allowed types: int, float, bool, str.
  • Auto-initialized to: int=0, float=0.0, bool=False, str="".
  • No name collisions. A slot cannot share a name with a Param, a Symbol, or a reserved strategy property → RC2_STATE_PARAM_CLASH / RC2_STATE_SYMBOL_CLASH / RC2_STATE_RESERVED.
  • self.x = [1, 2] is not how you get a list. Assigning a collection to a scalar slot inside execute() raises RC2_STATE_TYPE — collections are declared in the class body, and only there.

You do not initialize a scalar explicitly. The first read returns the zero value for the inferred type — the RaamCode equivalent of Python’s if not hasattr(self, "trade_count"): self.trade_count = 0, done for you.

Collections

A list or dict slot is declared in the class body with a literal, and that literal does two jobs: it pins the element (or value) type, and it is the state the slot is reset to at the start of every walk-forward window.

class Buffers(Strategy):
trade = Symbol()
sizing = FixedCapital()
recent_highs = [0.0]*5 # five decimals
session_lows = [999999.0, 999999.0] # written out longhand
counts = {"up": 0, "down": 0, "doji": 0} # string keys, int values
def execute(self):
for i in range(4):
self.recent_highs[i] = self.recent_highs[i+1]
self.recent_highs[4] = self.trade.high
if self.trade.close > self.trade.open:
self.counts["up"] += 1
else:
self.counts["down"] += 1
if self.trade.close > self.recent_highs[0] and self.trade.is_flat:
self.buy(self.trade)

Collection rules

len(self.<list>) and len(self.<dict>) work; iterating them is covered in For Loops.

Writing state from helpers

Helpers read persistent state freely, and may write into class-body lists and dicts by index or key. What they may not do is rebind a scalar slot — that write would not propagate back to the caller, so it is refused with RC2_HELPER_STATE_WRITE rather than silently lost.

Keep the scalar assignment in execute() and let the helper return the value:

class Counter(Strategy):
trade = Symbol()
sizing = FixedQuantity()
bullish_bars = 0.0
def execute(self):
self.bullish_bars = self.next_bullish_count()
if self.trade.is_flat and self.bullish_bars >= 3:
self.buy(self.trade)
def next_bullish_count(self):
if self.trade.close > self.trade.open:
return self.bullish_bars + 1
return 0

Collections are the exception, because a subscript write mutates the object the slot already points at:

class BufferedCounter(Strategy):
trade = Symbol()
sizing = FixedQuantity()
recent = [0.0]*3
def execute(self):
self.record()
if self.recent[2] > self.recent[0] and self.trade.is_flat:
self.buy(self.trade)
def record(self):
self.recent[0] = self.recent[1]
self.recent[1] = self.recent[2]
self.recent[2] = self.trade.close # ✅ index write from a helper
return 0

Walk-forward and state

When the optimizer runs walk-forward across consecutive windows, state resets at the start of each window — scalars back to their zero (or class-body) value, collections back to their declared literal. Treat persistent state as “remembered within one continuous run”, never as a long-term store.

State vs. Series

If what you want is “the last N values of a price or an indicator”, a Series inside a custom indicator is the purpose-built tool — see Custom Indicators. Persistent-state lists are for values a strategy accumulates: per-session extremes, counters, flags.

Common diagnostics

Code What it means
RC2_ASSIGN_LHS The left side isn’t a valid state target — or a subscript write into a slot that is a scalar, or was never declared.
RC2_ASSIGN_RHS The assignment has no right-hand side.
RC2_STATE_TYPE_REASSIGN A slot was reassigned to a different type.
RC2_STATE_SYMBOL_CLASH State shadows a Symbol().
RC2_STATE_PARAM_CLASH State shadows a Param().
RC2_STATE_RESERVED State shadows a platform property (capital, bar_count, …).
RC2_HELPER_STATE_WRITE A helper rebound a scalar slot.