Skip to main content

Strategies

A Strategy (tradeflow/strategies/base.py) is responsible for three things and no more:

  1. Indicatorsprocess_data(df) returns the OHLCV frame plus the columns it needs.
  2. Convictioncalculate_scores(df) returns one continuous, signed score per bar (positive = bullish, negative = bearish, magnitude = strength). This is the strategy's single source of truth.
  3. Sizing & riskcalculate_position_size, check_exit_conditions, validate_signal.

It does not fetch data, place orders, or compute portfolio metrics.

One score, two consumers

A strategy defines only the score. Everything else is derived from it, so there is no parallel decision path to keep in sync:

  • The trade clock gets its discrete BUY/SELL/HOLD from generate_signals(df), which the base class implements by walking the score with hysteresis (see below). Strategies do not override it.
  • The alpha layer (continuous alphas) reads the same score as a cross-sectional conviction and scales it into a residual-return forecast.

Deriving the signal from the score

generate_signals tracks the desired position direction implied by the score:

enter long when score crosses above enter_long
exit long when score falls to/below exit_long → CLOSE_BUY
(short side mirrors it, for strategies with LONG_ONLY = False)

The bands come from signal_thresholds() (default: pure sign — long while score > 0). A strategy with asymmetric entry/exit — e.g. mean_reversion enters when RSI is oversold but holds until it's overbought — overrides signal_thresholds() to set a wide hold band. Entries are edge-triggered (emitted on the crossing bar); while a direction is held the bar emits HOLD, and the engine dedupes against the open position.

The signal vocabulary

The derived signals are plain strings, defined once in tradeflow/strategies/signals.py so every layer agrees:

BUY, SELL # open a position
CLOSE_BUY, CLOSE_SELL # close a position
HOLD # do nothing

Keeping them central avoids the classic bug where one layer emits "buy" and another checks for "BUY".

Lifecycle hooks

MethodWhenPurpose
calculate_required_lookback()constructionbars needed before indicators are valid
initialize()start of a runvalidate parameters / relationships
process_data(df)each batch/baradd indicator columns
calculate_scores(df)after processingproduce {timestamp: score} (the one thing you implement)
signal_thresholds()per signal derivationoptional: asymmetric entry/exit bands
generate_signals(df)after scoring (base class)derive {timestamp: signal} from the score
process_real_time_data(...)live, per barfold a streamed bar into a rolling buffer and emit the latest signal

Position sizing

calculate_position_size(capital, price) derives a size from risk_per_trade and stop_loss, then clamps it to the configured limits (max_position_size, max_total_risk). It is the smallest of the three constraints — risk target, per- position notional cap, and total-risk cap.

Parameters & validation

Each strategy declares PARAM_RANGES with min/max/step/default/type. The base class coerces and range-checks every supplied value at construction, so an out-of-range parameter fails fast. step also lets the optimizer search the space.

The bundled strategies

Three ship today, all built on the pure indicators — chosen to disagree, so the walk-forward scorecard has something to discriminate between:

--strategyStyleScore (the one decision)
volume_spikeTrend + volume (5-minute, long/short)Signed EMA-trend strength × volume / volume_ma — lean with the trend, conviction amplified by volume confirmation.
ma_crossoverTrend (daily, long-only)Normalized EMA gap (fast − slow) / slow — sign crossings are the golden / death cross.
mean_reversionContrarian (daily, long-only)Oversold-ness 50 − rsi, with asymmetric bands: enter below oversold, exit above overbought.

ma_crossover and mean_reversion are deliberately minimal (a handful of parameters each) — honest baselines and clean worked examples. See Extending to add your own; it's a one-file change.