Helper Methods
You can define your own methods on the strategy class and call them from execute(). Helpers keep execute() readable when the logic gets dense.
class RiskManaged(Strategy): risk_pct = Param(1.0, min=0.5, max=5.0) stop_distance = Param(2.0, min=0.5, max=10.0) trade = Symbol() sizing = FixedQuantity()
def execute(self): sma = SMA(20, on=self.trade) self.stop_loss(self.stop_distance)
if self.trade.close > sma.value and self.trade.is_flat: shares = self.position_size_for_risk(self.risk_pct, self.stop_distance) self.buy(self.trade, qty=shares, reason="Risk-managed entry")
def position_size_for_risk(self, risk_pct, stop_pct): risk_amount = self.capital * risk_pct / 100 stop_amount = self.trade.close * stop_pct / 100 if stop_amount > 0: return int(risk_amount / stop_amount) return 0Signature rules
- First argument must be
self(RC2_HELPER_SIGNATURE). - All other arguments are positional and untyped.
- Every call must pass every declared parameter.
def my_helper(self, period, multiplier): # ✅ ...A default value parses, but does not make the argument optional
def my_helper(self, period, multiplier=1.5) is accepted by the parser, and then the arity check counts multiplier anyway — calling self.my_helper(20) is RC2_HELPER_ARITY. Write the parameters you intend callers to pass, and pass all of them.
What helpers can access
| Available | Notes |
|---|---|
| Params | self.fast, self.risk_pct, etc. |
| Symbols and OHLCV | self.trade.close, self.spy.high. |
| Position helpers | self.trade.is_long, etc. |
| Persistent state | Read and write self.my_counter. |
| Strategy properties | self.capital, self.bar_count. |
| Other helpers | Call them like any method. |
| Order verbs | self.buy(...), self.sell(...) work from helpers too. |
| Exit rules | self.stop_loss(...) works from helpers. |
What helpers cannot access
Indicator locals. Indicators live in execute()’s local scope. Helpers don’t see them.
def execute(self): rsi = RSI(14, on=self.trade) self.check_signal(rsi) # ✅ pass it in explicitly
def check_signal(self, rsi): if rsi.value < 30: # ✅ — passed-in indicator works fine return True return Falsedef execute(self): rsi = RSI(14, on=self.trade) self.check_signal() # tries to read rsi from helper scope
def check_signal(self): if rsi.value < 30: # ❌ — rsi is not in scope here return TrueIf a helper needs an indicator value, pass it as an argument. Same for any other local variable from execute().
Helpers and persistent state
Helpers read persistent state freely. What they cannot do is rebind a scalar slot: self.counter = … inside a helper does not propagate back to the caller, so rather than let the write vanish, the transpiler refuses it.
class Bad(Strategy): trade = Symbol() sizing = FixedQuantity() counter = 0
def execute(self): self.bump() if self.trade.is_flat: self.buy(self.trade)
def bump(self): self.counter = self.counter + 1 # ❌ scalar rebind inside a helper return 0Index and key writes into a class-body list or dict are allowed from a helper — those mutate through a reference both sides share, so the change survives the return:
class Good(Strategy): trade = Symbol() sizing = FixedQuantity() session_highs = [0.0]*5 counts = {"up": 0, "down": 0}
def execute(self): self.update_extremes() if self.trade.close > self.session_highs[0] and self.trade.is_flat: self.buy(self.trade)
def update_extremes(self): if self.trade.high > self.session_highs[0]: self.session_highs[0] = self.trade.high # ✅ index write — propagates self.counts["up"] += 1 # ✅ key write — propagates return 0If a helper really has to change a scalar, have it return the new value and let execute() do the assignment.
Common diagnostics
| Code | What it means |
|---|---|
RC2_HELPER_SIGNATURE |
First arg isn’t self. |
RC2_HELPER_ARITY |
Helper called with the wrong number of args. |
RC2_HELPER_STATE_WRITE |
Helper rebound a scalar state slot. Index and key writes into collections are fine. |