Core Concepts

Strategy Contract (Shape-C)

.md

Every new Finny strategy implements Shape-C — a Strategy class with on_bar, an injected broker, and explicit rules that prevent lookahead bias and unrealistic fills.

The Shape-C class

python
class Strategy:
    def __init__(self, broker, params=None):
        self.broker = broker
        self.params = params or {}

    def on_bar(self, symbol: str, bar: dict) -> None:
        # bar keys: open, high, low, close, volume, timestamp, symbol
        pass

The runner instantiates Strategy once and calls on_bar for every bar in the backtest window or live stream. The broker instance is injected — you never import or build one.

Broker API

buy(symbol, qty=None, notional=None)
Place a buy order. Pass qty or notional, not both.
sell(symbol, qty=None, notional=None)
Place a sell order. Same rules.
position(symbol)
Signed quantity. 0 means flat, negative is short.
cash()
Available cash for new orders.
equity()
Cash plus mark-to-market value of open positions.
price(symbol)
Last close — useful for sizing notional orders.

Two rules that trip up new strategies

Next-bar fill

Orders submitted inside on_bar fill at the next bar. Calling broker.buy() and then broker.position() in the same bar returns 0. Treat the position as updated only on the bar after the order.

Lookahead rule

Only bar["open"] is decision-time-safe. By the time you know high, low, or close, the bar is over — using them to decide an entry is lookahead bias and your backtest will lie to you.

Compute indicators from prior closes
Build your moving averages, RSI, Bollinger bands, etc. from the history of closes before the current bar — never from the current bar's close. The engine doesn't enforce this for you; the responsibility lives in the strategy.

Parameters

Read parameters from self.params with sensible defaults — that way a parameter sweep (see /backtest sweep) can actually vary them. Hard-coded constants make sweeps return identical metrics.

python
def on_bar(self, symbol, bar):
    rsi_period = self.params.get("rsi_period", 14)
    oversold = self.params.get("oversold", 30)
    # ...

Shape auto-detection

The backtest runner inspects the module to figure out which contract is in use:

  • A Strategy class whose __init__ takes broker as its first parameter → Shape C (current).
  • A legacy class name → Shape A, routed through a v1 compatibility adapter.
  • Neither → the runner aborts with a hint pointing at this page.

The Shape-C loader also accepts simpler strategy formats and maps them into the institutional engine_v2 — see the /backtest reference.