Quick Start: First Backtest in 5 Minutes¶
This tutorial demonstrates how to run your first backtest using SignalFlow's fluent builder API. We'll use synthetic data, so no API keys are needed.
What you'll learn:
- Generate synthetic market data
- Run a backtest with
sf.Backtest()fluent API - Inspect results with
BacktestResult - Use the
sf.backtest()one-liner shortcut
1. Setup¶
In [1]:
Copied!
from datetime import datetime
from pathlib import Path
import signalflow as sf
from signalflow.data import RawDataFactory
from signalflow.data.raw_store import DuckDbSpotStore
from signalflow.data.source import VirtualDataProvider
from datetime import datetime
from pathlib import Path
import signalflow as sf
from signalflow.data import RawDataFactory
from signalflow.data.raw_store import DuckDbSpotStore
from signalflow.data.source import VirtualDataProvider
2. Generate Synthetic Data¶
SignalFlow includes a VirtualDataProvider that generates realistic OHLCV data using a geometric random walk. This lets you develop and test strategies without API keys.
In [2]:
Copied!
# Create a DuckDB store and generate 10,000 one-minute bars
db_path = Path("/tmp/quickstart.duckdb")
store = DuckDbSpotStore(db_path=db_path)
VirtualDataProvider(store=store, seed=42).download(
pairs=["BTCUSDT", "ETHUSDT"],
n_bars=10_000,
)
print(f"Generated data at {db_path}")
# Create a DuckDB store and generate 10,000 one-minute bars
db_path = Path("/tmp/quickstart.duckdb")
store = DuckDbSpotStore(db_path=db_path)
VirtualDataProvider(store=store, seed=42).download(
pairs=["BTCUSDT", "ETHUSDT"],
n_bars=10_000,
)
print(f"Generated data at {db_path}")
2026-02-15 00:49:25.111 | INFO | signalflow.data.raw_store.duckdb_stores:_ensure_tables:153 - Database initialized: /tmp/quickstart.duckdb (data_type=spot, timeframe=1m) 2026-02-15 00:49:25.205 | DEBUG | signalflow.data.raw_store.duckdb_stores:insert_klines:220 - Inserted 10,000 rows for BTCUSDT 2026-02-15 00:49:25.206 | INFO | signalflow.data.source.virtual:download:255 - VirtualDataProvider: generated 10000 bars for BTCUSDT 2026-02-15 00:49:25.278 | DEBUG | signalflow.data.raw_store.duckdb_stores:insert_klines:220 - Inserted 10,000 rows for ETHUSDT 2026-02-15 00:49:25.279 | INFO | signalflow.data.source.virtual:download:255 - VirtualDataProvider: generated 10000 bars for ETHUSDT
Generated data at /tmp/quickstart.duckdb
3. Load Data¶
In [3]:
Copied!
raw_data = RawDataFactory.from_duckdb_spot_store(
spot_store_path=db_path,
pairs=["BTCUSDT", "ETHUSDT"],
start=datetime(2020, 1, 1),
end=datetime(2030, 1, 1),
)
print(f"Loaded {len(raw_data.pairs)} pairs")
print(f"Spot data shape: {raw_data.get('spot').shape}")
raw_data = RawDataFactory.from_duckdb_spot_store(
spot_store_path=db_path,
pairs=["BTCUSDT", "ETHUSDT"],
start=datetime(2020, 1, 1),
end=datetime(2030, 1, 1),
)
print(f"Loaded {len(raw_data.pairs)} pairs")
print(f"Spot data shape: {raw_data.get('spot').shape}")
2026-02-15 00:49:25.294 | INFO | signalflow.data.raw_store.duckdb_stores:_ensure_tables:153 - Database initialized: /tmp/quickstart.duckdb (data_type=spot, timeframe=1m)
Loaded 2 pairs Spot data shape: (20000, 8)
4. Run Backtest with Fluent API¶
The sf.Backtest() builder provides a clean, chainable API for configuring backtests:
In [4]:
Copied!
result = (
sf.Backtest("quickstart")
.data(raw=raw_data)
.detector("example/sma_cross", fast_period=20, slow_period=50)
.exit(tp=0.03, sl=0.015)
.capital(50_000)
.run()
)
result = (
sf.Backtest("quickstart")
.data(raw=raw_data)
.detector("example/sma_cross", fast_period=20, slow_period=50)
.exit(tp=0.03, sl=0.015)
.capital(50_000)
.run()
)
2026-02-15 00:49:25.333 | DEBUG | signalflow.core.registry:_discover_internal_packages:152 - autodiscover: failed to import signalflow.detector.adapter Backtesting: 100%|██████████| 10000/10000 [00:00<00:00, 31057.58it/s]
5. Inspect Results¶
In [5]:
Copied!
print(result.summary())
print(result.summary())
==================================================
BACKTEST SUMMARY
==================================================
Trades: 444
Win Rate: 0.0%
Profit Factor: 0.00
--------------------------------------------------
Initial Capital: $ 50,000.00
Final Capital: $ 0.00
Total Return: -100.0%
--------------------------------------------------
==================================================
/home/alastor/sf-project/sf/src/signalflow/api/result.py:398: UserWarning: Using default source 'default' for 'spot'. Specify explicitly: raw.spot.default spot = accessor.to_polars()
In [6]:
Copied!
# Access individual metrics
print(f"Number of trades: {result.n_trades}")
print(f"Win rate: {result.win_rate:.1%}")
print(f"Total return: {result.total_return:.2%}")
print(f"Profit factor: {result.profit_factor:.2f}")
# Access individual metrics
print(f"Number of trades: {result.n_trades}")
print(f"Win rate: {result.win_rate:.1%}")
print(f"Total return: {result.total_return:.2%}")
print(f"Profit factor: {result.profit_factor:.2f}")
Number of trades: 444 Win rate: 0.0% Total return: -100.00% Profit factor: 0.00
In [7]:
Copied!
# Export trades as DataFrame
trades_df = result.to_dataframe()
if trades_df.height > 0:
print(trades_df.head(5))
else:
print("No trades executed (try adjusting detector parameters)")
# Export trades as DataFrame
trades_df = result.to_dataframe()
if trades_df.height > 0:
print(trades_df.head(5))
else:
print("No trades executed (try adjusting detector parameters)")
shape: (5, 1) ┌─────────────────────────────────┐ │ trade │ │ --- │ │ str │ ╞═════════════════════════════════╡ │ Trade(id='02e7f1b7-e1e1-4bb5-a… │ │ Trade(id='d4fba9b1-4d0c-4676-8… │ │ Trade(id='28aa35a1-d3ec-4613-b… │ │ Trade(id='aea31fb9-7fa8-4a38-8… │ │ Trade(id='5fe814e1-f502-449a-8… │ └─────────────────────────────────┘
6. One-Liner Shortcut¶
For quick experiments, use the sf.backtest() shortcut:
In [8]:
Copied!
from signalflow.detector import ExampleSmaCrossDetector
quick_result = sf.backtest(
detector=ExampleSmaCrossDetector(fast_period=10, slow_period=30),
raw=raw_data,
tp=0.02,
sl=0.01,
capital=10_000,
)
print(quick_result.summary())
from signalflow.detector import ExampleSmaCrossDetector
quick_result = sf.backtest(
detector=ExampleSmaCrossDetector(fast_period=10, slow_period=30),
raw=raw_data,
tp=0.02,
sl=0.01,
capital=10_000,
)
print(quick_result.summary())
Backtesting: 100%|██████████| 10000/10000 [00:00<00:00, 20141.23it/s]
==================================================
BACKTEST SUMMARY
==================================================
Trades: 742
Win Rate: 0.0%
Profit Factor: 0.00
--------------------------------------------------
Initial Capital: $ 10,000.00
Final Capital: $ 0.00
Total Return: -100.0%
--------------------------------------------------
==================================================
7. Clean Up¶
In [9]:
Copied!
store.close()
db_path.unlink(missing_ok=True)
print("Done!")
store.close()
db_path.unlink(missing_ok=True)
print("Done!")
Done!
Next Steps¶
- 02 - Custom Detector: Create your own signal detector
- 03 - Data Loading & Resampling: Work with multiple timeframes
- 04 - Pipeline Visualization: Visualize your strategy pipeline
- 05 - Advanced Strategies: Multi-detector ensembles