01 - Quick Start¶
Build your first Flow end to end on the built-in memory source - no API keys, no network. You will:
- build a Dataset
- train a ForecastModel (SMA feature pipe + FixedHorizon target)
- assemble a Flow (ThresholdDetector + RulesStrategy)
- backtest it and read the scorecard
- round-trip the whole stack through YAML (deploy is data)
import signalflow as sf
sf.__version__
'0.8.4'
1. Build a Dataset¶
sf.data("memory", ...) creates a lazy, immutable OHLCV container. The same object feeds backtest, paper, and live.
ds = sf.data("memory", pairs=["BTCUSDT"], start="2023-01-01", interval="1h")
ds
Dataset(frame=shape: (5_000, 7)
┌─────────┬─────────────────────┬────────────┬────────────┬────────────┬────────────┬────────────┐
│ pair ┆ ts ┆ open ┆ high ┆ low ┆ close ┆ volume │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ datetime[ms] ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞═════════╪═════════════════════╪════════════╪════════════╪════════════╪════════════╪════════════╡
│ BTCUSDT ┆ 2022-12-31 22:00:00 ┆ 137.0 ┆ 137.058608 ┆ 136.636811 ┆ 136.687537 ┆ 152.239364 │
│ BTCUSDT ┆ 2022-12-31 23:00:00 ┆ 136.687537 ┆ 136.884798 ┆ 136.420861 ┆ 136.70825 ┆ 104.41044 │
│ BTCUSDT ┆ 2023-01-01 00:00:00 ┆ 136.70825 ┆ 137.379643 ┆ 136.511785 ┆ 137.125962 ┆ 111.79902 │
│ BTCUSDT ┆ 2023-01-01 01:00:00 ┆ 137.125962 ┆ 137.532054 ┆ 136.993873 ┆ 137.072091 ┆ 248.527547 │
│ BTCUSDT ┆ 2023-01-01 02:00:00 ┆ 137.072091 ┆ 137.426566 ┆ 137.03899 ┆ 137.127774 ┆ 159.097595 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ BTCUSDT ┆ 2023-07-28 01:00:00 ┆ 152.078116 ┆ 152.452532 ┆ 151.965199 ┆ 152.279535 ┆ 150.798506 │
│ BTCUSDT ┆ 2023-07-28 02:00:00 ┆ 152.279535 ┆ 153.229599 ┆ 152.248538 ┆ 152.828584 ┆ 119.882388 │
│ BTCUSDT ┆ 2023-07-28 03:00:00 ┆ 152.828584 ┆ 153.333118 ┆ 152.300961 ┆ 152.963132 ┆ 101.004053 │
│ BTCUSDT ┆ 2023-07-28 04:00:00 ┆ 152.963132 ┆ 153.125397 ┆ 152.615698 ┆ 152.795202 ┆ 103.793203 │
│ BTCUSDT ┆ 2023-07-28 05:00:00 ┆ 152.795202 ┆ 152.836269 ┆ 152.230447 ┆ 152.430256 ┆ 224.547178 │
└─────────┴─────────────────────┴────────────┴────────────┴────────────┴────────────┴────────────┘, source_name='memory', source_params={'pairs': ['BTCUSDT'], 'start': '2023-01-01', 'end': None, 'interval': '1h'}, quote='USDT', provenance=<Provenance.FULL: 'full'>)
2. Train a ForecastModel¶
A ForecastModel pairs a Target (what to predict) with a FeaturePipe (the inputs). fit trains it out-of-fold with an embargo, so its out-of-sample predictions are leak-free.
model = sf.ForecastModel(
target=sf.FixedHorizon(bars=12),
features=sf.FeaturePipe(sf.SMA(10), sf.SMA(20), sf.SMA(50)),
)
model.fit(ds)
2026-07-08 03:02:40.344 | DEBUG | signalflow.model.forecast:fit:163 - ForecastModel fitted: oos rows=4855, folds=206
ForecastModel(backend='lightgbm', output='p_rise', fitted)
predict is the production path; predict_oos returns values only inside the trained out-of-sample span (null elsewhere), which is what keeps evaluation honest.
oos = model.predict_oos(ds)
oos.head()
2026-07-08 03:02:40.351 | WARNING | signalflow.model.forecast:predict_oos:315 - predict_oos: 145 rows outside cached OOS span (null)
| pair | ts | p_rise |
|---|---|---|
| str | datetime[ms] | f64 |
| "BTCUSDT" | 2022-12-31 22:00:00 | null |
| "BTCUSDT" | 2022-12-31 23:00:00 | null |
| "BTCUSDT" | 2023-01-01 00:00:00 | null |
| "BTCUSDT" | 2023-01-01 01:00:00 | null |
| "BTCUSDT" | 2023-01-01 02:00:00 | null |
3. Assemble a Flow¶
A Flow wires forecasts -> detectors -> strategy. The ThresholdDetector fires when the rise forecast clears p_min; RulesStrategy turns signals into trades. Every forecast slot must hold a trained model.
flow = sf.Flow(
name="sma_rise",
forecasts={"rise": model},
detectors=[sf.ThresholdDetector(forecast="rise", p_min=0.6)],
strategy=sf.RulesStrategy(),
)
flow
Flow('sma_rise', forecasts=['rise'], detectors=['threshold'], validator=none)
4. Backtest¶
flow.backtest returns a Run. run.scorecard() is the standard metric dict.
run = flow.backtest(ds, capital=50_000)
run.scorecard()
{'name': 'sma_rise',
'mode': 'backtest',
'target': 'USDT',
'promotable': False,
'oos': False,
'n_fills': 16,
'initial_equity': 50000.0,
'final_equity': 50203.15,
'total_return': 0.0041,
'max_drawdown': 0.0055,
'sharpe': 0.86}
5. Deploy is data¶
flow.save writes the wiring to YAML plus a model directory; sf.Flow.load restores a byte-identical backtest. Promoting a strategy is moving a file.
flow.save("flow.yaml", model_dir="models")
same = sf.Flow.load("flow.yaml")
same.backtest(ds, capital=50_000).final_equity == run.final_equity
True
The reloaded flow reproduces the backtest exactly. Next: the Concepts page for the invariants, and Live & Walk-Forward for the walk-forward workflow.