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
intand later afloatto the same slot raisesRC2_STATE_TYPE_REASSIGN. The other way round is fine: a whole number into afloatslot is stored as a float (self.level = 0afterlevel = 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, aSymbol, 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 insideexecute()raisesRC2_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
- One level deep. A list of lists, or a dict of dicts, raises
RC2_STATE_NESTED. - One element type.
[1, 2.0]raisesRC2_STATE_MIXED; the same goes for dict values. - Dict keys are string literals →
RC2_STATE_DICT_KEY. - No bare
[]or{}— there would be no type to infer. Use[0.0]*0or{"_": 0.0}→RC2_STATE_EMPTY_LIST/RC2_STATE_EMPTY_DICT. - Mutate, don’t rebind.
self.recent_highs = [...]insideexecute()raisesRC2_STATE_REBIND_COLLECTION; write elements instead. - Augmented assignment needs the slot to exist →
RC2_STATE_AUGASSIGN_UNINIT.
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 0Collections 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 0Walk-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. |
Related
- For Loops — iterating the collections declared here.
- Helper Methods
- Rolling Channel Breakout — a worked example built entirely on these features.