The full quant workflow in one framework: build strategies, vector & event-driven backtest at scale, compare in a single dashboard, and deploy the winner 🚀
v9.0.0 alpha is out! The pre-release of v9.0 is now available on PyPI as an alpha pre-release. Since pip doesn't install pre-releases by default, pin the version explicitly or pass
--pre:pip install investing-algorithm-framework==9.0.0a12You can find the blog post here: v9.0 Release.
The API examples and feature list below describe current v9 development. Older published alpha versions may not include every API shown.
Investing Algorithm Framework is a Python framework that covers the entire quant workflow: define a strategy once, vector-backtest thousands of parameter variants to find promising signals, narrow down with a storage layer that ranks 10k+ results in milliseconds, validate the winners in a realistic event-driven simulation, compare everything in a single interactive HTML dashboard, and deploy the best performer live, all with the same TradingStrategy class, no code rewrites between stages.
Most quant frameworks stop at "here's your backtest result." You get a number, maybe a chart, and then you're on your own figuring out which strategy variant is actually better, whether the result is robust across time windows, and how to go from research to production. This framework closes that gap.
Want to see this in practice? Check out the
examples/tutorial/: a series of runnable notebooks that walk you through every stage: defining a strategy, visualizing its signals, sweeping parameters across rolling windows, detecting overfitting with Monte Carlo permutation tests, filtering and ranking with the storage layer, and deploying the winner.
Both backtesting engines always return a disk-backed BacktestIndex, including
single-strategy runs. Full results are saved persistently and loaded explicitly
with index.iter_backtests() or index.load_backtests(). For large sweeps,
both engines support bounded parallel workers and optional soft memory budgets.
See memory-budgeted sweeps
for progressive pruning, streaming result loading, and Windows/WSL safeguards.
What's New in v9.0
Full details: v9.0 release notes · CHANGELOG · OBTF spec
- New Open Backtest Format (.OBTF): OBTF packs studies, universes, windows, vector/event runs, summaries, metrics, trades, orders, positions, snapshots, execution assumptions and Monte Carlo tests into a single versioned
.obtffile per algorithm (zstd + MessagePack + Parquet under the hood), so your results are portable, future-proof, and never scattered across folders again. - Dual engine native: vector and event engines now run as first-class citizens of every backtest, so you can sweep thousands of signal ideas and validate the winners under realistic execution in the same bundle, with zero risk of one engine's save wiping out the other's results. Now a .obtf bundle is a complete record of your vector and event backtests of a single strategy.
- Configuration-based backtest API:
BacktestRunConfigurationcentralizes execution settings, with checkpoints, continue-on-error and progress enabled by default, plus.from_env()support. Both app backtest methods return persistentBacktestIndexresults and support scalar metrics filters. - Pluggable backtest optimization: search existing algorithm collections or generate parameterized strategies through
OptimizationConfigurationand your ownStrategyOptimizer. Reuse bounded workers, memory controls, ID/window checkpoints and durable optimizer-state resume. Base classes and orchestration are included; concrete optimizers are supplied by the user. See the optimizer guide. - Short and Long Signals support: a couple of new methods (
generate_short_signals/generate_cover_signals) are all it takes to unlock full short-selling: SHORT/COVER order routing, correct P&L and collateral handling, and fill-based trade creation across vector, event, and live trading. - Enhanced Study definitions: reusable
Study,UniverseandBacktestWindowbuilding blocks give you rolling, anchored, holdout and walk-forward k-fold validation, cross-sectional pipelines, signal cooldowns, and Monte Carlo–backed ranking, so you can trust your edge before you trade it. - *Custom commision nd slippage models: pluggable slippage and commission models (percentage, fixed, bps, volume-aware) snapshot every study's cost assumptions via
ExecutionConfigand attribute fees down to the order and trade level, so your numbers hold up in the real world. - State of the art backtest storage and indexings: a swappable
BacktestStore, SQLite indexing across engines, studies, universes and lineage, content-addressed OHLCV deduplication, and a fulliafCLI for migrating, indexing, ranking and pruning results, so a growing research pipeline never becomes a mess. - See more, faster: an expanded metrics suite (CAGR, Sharpe, Sortino, Calmar, VaR/CVaR, drawdown/recovery, benchmark comparisons) paired with per-engine, per-study HTML reports and pooled or per-universe summaries, so you spot the winning strategy at a glance.
- Portfolio sync operations: recurring or one-off per-market deposit schedules, environment-based credential resolution make it easier to manage live portfolios.
⚠️ v9.0 is an alpha release with breaking API and persisted-data changes from v8. Legacy readers and selected compatibility properties remain available to assist migration, but new output is written in the v9 OBTF model — validate strategy behavior, execution assumptions and stored backtests before adopting in production.
Features
- 🔁 Long & Short signals support for Live trading & Backtesting: Build strategies as a pipeline of entry/exit signals, position sizing, and order generation, each independently overridable. Long-only by default; opt into shorts by overriding two methods. The same strategy class runs unchanged in vector backtests, event-driven backtests, and live.
- 🗂️ Open Backtest Format storage — One
{algorithm_id}.ofbtper algorithm holds every study (in-sample sweep, time-OOS, universe-OOS, walk-forward, stress test) as a first-class slot with its own universe, windows, engine runs and summary. See Open-Backtest-Format for the reference spec. - 📊 30+ Metrics: CAGR, Sharpe, Sortino, Calmar, VaR, CVaR, Max DD, Recovery & more
- 🧮 Cross-Sectional Pipelines: Rank, filter and score entire universes of symbols every iteration with a tidy factor table
- ⚡ Vector Backtesting for Signal Analysis: Quickly test your strategy logic on historical data to see how signals would have behaved before committing to full event-driven backtests
- 🏃 Event-Driven Backtesting: Once promising strategies are identified via vector backtests, run full event-driven backtests to simulate realistic execution and portfolio management
- Pluggable Optimization: Budget candidate evaluations, plug in an ask/tell search policy, and resume both search state and completed event/vector evaluations without implementing your own backtest scheduler.
- 🔀 Permutation Testing / Monte Carlo Simulations: Assess the statistical robustness of your strategies by running them across randomized market scenarios to see how often your results could occur by chance
- 🚀 Deployment: Once the best strategy is identified through backtesting and comparison, deploy it to production locally or in the cloud (AWS Lambda / Azure Functions) to start live trading
- ⚔️ Multi-Strategy Comparison: Rank, filter & compare strategies in a single interactive report
- 🪟 Multi-Window Robustness: Test across different time periods with window coverage analysis
- 📈 Charts & Performance Analysis: Equity curves, rolling Sharpe, drawdown & return distributions, monthly heatmaps, yearly returns, and good/average/bad/very-bad return scenario projections — all rendered side-by-side per strategy
- 📉 Benchmark Comparison: Beat-rate analysis vs Buy & Hold, DCA, risk-free & custom benchmarks
- 📄 One-Click HTML Report: Self-contained file, no server, dark & light theme, shareable
- 🗄️ Tiered Backtest Storage Layer: Manage thousands of
.obtfbundles with a Tier-1 SQLite index (sub-100 ms ranks/filters over 10k+ backtests), a swappableBacktestStoreprotocol (LocalDirStore,LocalTieredStore), content-addressed Tier-3 OHLCV deduplication, and a CLI (iaf index/iaf list/iaf rank/iaf migrate-store) that plugs straight into the HTML dashboard. - 🌐 Load External Data: Fetch CSV, JSON, or Parquet from any URL with caching and auto-refresh
- 🪙 Per-Market Deposit Schedules & Portfolio Sync: Declare recurring or one-shot external cash flows on a market with
deposit_schedule=/auto_sync=True. Backtests simulate the deposits; live mode reconciles with the broker — samecontext.sync_portfolio()API in both modes. - 📝 Record Custom Variables — Track any indicator or metric during backtests with
context.record() - ⏱️ Signal Cooldowns: Throttle whipsaw with declarative
CooldownRules: per-symbol or portfolio-wide, side-aware (trigger="sell",blocks="buy"), enforced identically by the vector and event-driven engines
Strategy Definition
Declare what data your strategy needs and when to buy or sell as a TradingStrategy subclass — the framework wires up data loading, signal evaluation, order execution, position management, and reporting around it. The same class runs unchanged in vector backtests, event-driven backtests, paper trading and live.
Want strategy ideas to start from? Check out
examples/strategies_showcase/: a collection of runnable strategy templates (trend following, mean reversion, cross-sectional momentum, multi-factor, pairs trading, and more).
Risk and execution behaviour are expressed as declarative rule lists rather than ad-hoc code paths, so the engines can enforce them identically across modes:
position_sizes:PositionSizeper symbol (fixed amount or percentage of portfolio).stop_losses/take_profits:StopLossRule/TakeProfitRulewith fixed or trailing thresholds and partial-exitsell_percentage.scaling_rules:ScalingRulefor pyramiding (scale_in_percentage=[…],max_entries, per-symbolcooldown_in_bars).exposure_rule:ExposureRulecaps total invested value across the whole portfolio (e.g. never more than 80% invested) — portfolio-wide, unlike the per-symbol rules above.cooldowns:CooldownRuleto throttle whipsaw — per-symbol or portfolio-wide, side-aware (e.g.trigger="sell", blocks="buy", bars=12). Enforced bar-for-bar in both the vector and event-driven engines.
Fees and slippage (TradingCost per symbol) are configured separately on PortfolioConfiguration/app.add_market(trading_costs=[...]) (live/paper) or Study.execution_config (backtests), since they're a property of the venue/scenario, not of the strategy's signal logic.
from investing_algorithm_framework import (
TradingStrategy,
PositionSize,
ScalingRule,
ExposureRule,
StopLossRule,
TakeProfitRule,
CooldownRule,
SignalSide,
)
class MyStrategy(TradingStrategy):
symbols = ["BTC", "ETH"]
# Portfolio-wide: never invest more than 80% of the portfolio at
# once, across every symbol combined.
exposure_rule = ExposureRule(max_portfolio_percentage=80.0)
position_sizes = [
PositionSize(symbol="BTC", percentage_of_portfolio=20),
PositionSize(symbol="ETH", percentage_of_portfolio=20),
]
stop_losses = [
StopLossRule(symbol="BTC", percentage_threshold=5, trailing=True),
StopLossRule(symbol="ETH", percentage_threshold=5, trailing=True),
]
take_profits = [
TakeProfitRule(
symbol="BTC", percentage_threshold=10, sell_percentage=50,
),
TakeProfitRule(
symbol="ETH", percentage_threshold=10, sell_percentage=50,
),
]
scaling_rules = [
ScalingRule(
symbol="BTC", max_entries=3, scale_in_percentage=[50, 25],
),
ScalingRule(
symbol="ETH", max_entries=3, scale_in_percentage=[50, 25],
),
]
cooldowns = [
CooldownRule(symbol="BTC", trigger="sell", blocks="buy", bars=12),
CooldownRule(trigger="any", blocks="any", bars=2),
]
def generate_signals(self, context, data):
"""Event-mode entry point — live, paper trading, event backtests."""
...
# yield Signal(symbol="BTC", side=SignalSide.OPEN_LONG, source="my_rule")
# yield Signal(symbol="BTC", side=SignalSide.CLOSE_LONG, source="my_rule")
# Optional — short selling is opt-in: also yield OPEN_SHORT / CLOSE_SHORT.
def generate_signal_series(self, data):
"""Vector-mode entry point — only needed for vector backtests."""
...
# yield SignalSeries(symbol="BTC", side=SignalSide.OPEN_LONG, series=entry_series)Backtesting Engines
Use Study for the experiment and BacktestRunConfiguration for execution.
With an app, data providers, strategies and training study already configured:
from investing_algorithm_framework import BacktestRunConfiguration
run_configuration = BacktestRunConfiguration(
backtest_storage_directory="./my-backtests",
continue_on_error=True,
use_checkpoints=True,
show_progress=True,
n_workers=8,
memory_budget_mb=16_384, # Soft 16 GiB process-tree RSS budget
min_available_memory_mb=4_096, # Keep 4 GiB of available headroom
max_tasks_per_child=16,
)
results = app.run_backtests(
strategies=strategies,
study=training_study,
run_configuration=run_configuration,
)
print(results.df) # Scalar metrics; no full bundle loading
print(results.directory)- Use
strategy=oralgorithm=withapp.run_backtest()for a single candidate, orstrategies=/algorithms=for independent candidates. - Both methods always return a disk-backed
BacktestIndex, not a list ofBacktestobjects. Load selected full results explicitly withresults.iter_backtests()orresults.load_backtests(). - Checkpoints, continue-on-error and progress default to
True.BacktestRunConfiguration.from_env()readsIAF_BACKTEST_*settings. - Put worker, memory, snapshot and data-preparation settings inside the
configuration, not directly on the app call.
result_modeanditerative_summary_updateare no longer public options. - Keep
window_metrics_filter_functionandfinal_metrics_filter_functionon the app call. They receive and return indexes; window summaries are always current. - Reuse the same storage directory to resume. Checkpoints match only algorithm IDs and window IDs, so use a different directory when the experiment's data, strategy behavior or assumptions change.
See memory-budgeted sweeps for resource safeguards and filter examples. Memory limits are soft admission controls, not hard OS allocation limits.
Polars-powered vectorized signal evaluation. Compare thousands of strategies side by side, sweep parameter grids, run multi-window robustness checks, rank by key metrics and surface your top candidates in seconds — all before committing to a full event-driven simulation.
Once you've narrowed down promising strategies, run them through a full event-driven simulation. Pluggable slippage and fill models, partial fills, and a complete simulation blotter — using the same code path you'll deploy live.
Backtest Optimization
Add optimization= to either app backtest method to select from an existing
candidate collection or generate new parameterized strategies. The optimizer
chooses what to evaluate; the framework runs and scores the candidates using
the same event/vector engines and resource controls.
For an existing collection with unique algorithm IDs, supply your own optimizer
instance supporting "finite" search spaces:
import math
from investing_algorithm_framework import OptimizationConfiguration
def score_candidate(index):
pooled = index.df.loc[index.df["universe_key"].isna()]
if len(pooled) != 1:
raise ValueError("Expected one pooled candidate row")
score = float(pooled["summary.sharpe_ratio"].iloc[0])
if not math.isfinite(score):
raise ValueError("Candidate has no finite Sharpe ratio")
return score
results = app.run_backtests(
algorithms=my_algorithms,
study=training_study,
run_configuration=run_configuration,
optimization=OptimizationConfiguration(
search_id="algorithm-search-v1",
optimizer=my_optimizer,
objective=score_candidate,
direction="maximize",
max_evaluations=100,
max_proposals=1_000,
proposal_batch_size=16,
),
)my_algorithms and my_optimizer are user-supplied; no concrete search algorithm
is selected automatically. To generate candidates instead, omit the collection
and configure strategy_factory, IntegerParameter / FloatParameter
definitions and optional constraints with a parameter-capable optimizer.
Implement the StrategyOptimizer lifecycle: initialize, ask, tell,
is_finished, state_dict and load_state_dict. No random, grid, CryStAl or
Bayesian optimizer implementation is bundled.
The result remains a search-wide BacktestIndex. Trials and optimizer snapshots
are saved under <storage-root>/optimizations/<search_id>/. Resume with the same
configured storage root, search ID and unchanged experiment inputs; worker and
memory settings may change. The returned index directory is the nested search
directory, not the storage root.
proposal_batch_size controls search admission, not worker count. An optimizer
can save time by evaluating fewer candidates, but does not make an individual
backtest faster or guarantee better out-of-sample performance. Distributed
execution is not included in this API.
See Backtest Optimization for both candidate modes, the plugin contract, filters and recovery details.
Backtest Analysis & Dashboard
Every backtest produces a self-contained HTML dashboard — open it in any browser, share with teammates, archive it. No server, no Jupyter, no dependencies. Compare strategies side-by-side, drill into trades, and capture your reasoning as you go.
- Self-contained HTML reports — equity curves, drawdowns, trade lists, monthly returns, side-by-side strategy comparison
- Built-in MCP server — let Copilot, Claude, or any MCP-compatible agent query your backtests, rank strategies, and reason over trades through
investing-algorithm-framework mcp - Notes keeping — annotate every backtest with hypotheses, observations and conclusions; notes travel with the report so your research is never lost
💡 Want state-of-the-art analytics, publishable reports, ranking across thousands of runs and AI agents that do the analysis for you? Partner with our analytics integration partners below — they pick up where the local
report.htmlleaves off.
Every app backtest API, vector or event-driven, returns a disk-backed
BacktestIndex. Select candidates using its scalar rows, then explicitly load
the full Backtest objects that BacktestReport consumes:
from investing_algorithm_framework import (
BacktestReport, BacktestRunConfiguration, Study, Universe,
BacktestWindow, BacktestEngine,
)
# --- Single event-driven backtest ---
event_study = Study(
universe=Universe(market="BITVAVO", trading_symbol="EUR"),
initial_capital=1000,
backtest_windows=[BacktestWindow(train_range=date_range)],
engines=[BacktestEngine.EVENT_DRIVEN],
)
backtests = app.run_backtest(
strategy=strategy,
study=event_study,
run_configuration=BacktestRunConfiguration(
backtest_storage_directory="./event-backtests",
),
)
BacktestReport(
backtests=backtests.load_backtests(workers=1),
).save("event_report.html")
# --- A sweep of vector backtests (parameter grid / multi-window) ---
sweep_study = Study(
universe=Universe(market="BITVAVO", trading_symbol="EUR"),
initial_capital=1000,
backtest_windows=[
BacktestWindow(train_range=dr)
for dr in [range_2022, range_2023, range_2024]
],
engines=[BacktestEngine.VECTOR],
)
backtests = app.run_backtests(
strategies=[StrategyA(), StrategyB(), StrategyC()],
study=sweep_study,
run_configuration=BacktestRunConfiguration(
n_workers=8,
backtest_storage_directory="./my-backtests/",
memory_budget_mb=16_384,
min_available_memory_mb=4_096,
),
)
# Only materialize a suitably small selection for the dashboard.
BacktestReport(
backtests=backtests.load_backtests(workers=1),
).save("sweep_report.html")
# --- Or: load a folder of bundles back later (parallel decode) ---
report = BacktestReport.open(
directory_path="./my-backtests/",
workers=-1,
show_progress=True,
)
report.save("from_disk_report.html")For sweeps that grow into the thousands, combine this with the Backtest Storage Layer below — rank in SQLite first, then load only the winners into the report:
from investing_algorithm_framework import BacktestReport
from investing_algorithm_framework.cli.index_command import (
build_index, rank_index,
)
from investing_algorithm_framework.services.backtest_store import (
LocalDirStore,
)
# 1. Build (or refresh) the Tier-1 SQLite index over the folder of bundles.
build_index("./my-backtests/")
# 2. Pick the top 25 by Sharpe straight from SQLite — no Parquet decoded.
top = rank_index(
"./my-backtests/",
by="sharpe_ratio",
where="summary_number_of_trades > 50",
limit=25,
)
# 3. Materialise only those 25 bundles through the BacktestStore protocol.
store = LocalDirStore("./my-backtests/")
winners = [store.open(row["bundle_path"]) for row in top]
# 4. Render a focused dashboard with just the winners.
BacktestReport(backtests=winners).save("top25_by_sharpe.html")Backtest Storage Layer — scale to thousands of backtests
Once you start sweeping parameter grids and walk-forward windows, a flat folder of .obtf bundles stops scaling: every comparison re-decodes multi-MB Parquet metric blobs just to read a Sharpe number. The storage layer fixes that with three tiers behind a single BacktestStore protocol:
- Tier-1 — SQLite index (
index.sqlite): one row per bundle with every scalar fromBacktestSummaryMetricspromoted to its own column. Ranking 10k+ bundles becomes a sub-100 ms SQL query — no.obtfis opened. - Tier-2 —
BacktestStoreadapters:LocalDirStore(flat folder of bundles) orLocalTieredStore(hive-partitioned layout). Same handle-based API, swap the implementation without touching call sites. - Tier-3 — content-addressed OHLCV chunks: SHA-256 deduped per-symbol OHLCV blobs shared across every bundle that references them.
garbage_collect_ohlcv()reclaims orphans.
A CLI ties it all together: iaf index builds/refreshes the Tier-1 SQLite, iaf list / iaf rank query it, and iaf migrate-store moves a whole collection between store kinds in one command.
from investing_algorithm_framework import BacktestReport
from investing_algorithm_framework.cli.index_command import (
build_index, rank_index,
)
from investing_algorithm_framework.services.backtest_store import (
LocalDirStore,
)
# 1. Build (or refresh) the Tier-1 SQLite index over a folder of .obtf bundles.
build_index("./my-backtests/") # equivalent to: iaf index ./my-backtests/
# 2. Pick the top 20 by Sharpe straight from SQLite — no Parquet decoded.
top = rank_index(
"./my-backtests/",
by="sharpe_ratio",
where="summary_number_of_trades > 50",
limit=20,
)
# 3. Materialise just those 20 bundles through the BacktestStore protocol.
store = LocalDirStore("./my-backtests/")
backtests = [store.open(row["bundle_path"]) for row in top]
# 4. Feed them straight into the HTML dashboard.
BacktestReport(backtests=backtests).save("top20.html")Instead of sorting by a single column, use a focus preset to score every bundle across multiple metrics at once — profit, risk, consistency, win rate — weighted by what matters most to your workflow:
from investing_algorithm_framework import (
BacktestReport, BacktestEvaluationFocus,
)
from investing_algorithm_framework.cli.index_command import (
build_index, rank_index,
)
from investing_algorithm_framework.services.backtest_store import (
LocalDirStore,
)
# 1. Build (or refresh) the Tier-1 SQLite index.
build_index("./my-backtests/")
# 2. Rank with a built-in focus preset (BALANCED, PROFIT, FREQUENCY, RISK_ADJUSTED).
top = rank_index(
"./my-backtests/",
focus=BacktestEvaluationFocus.RISK_ADJUSTED,
where="summary_number_of_trades > 50",
limit=25,
)
# 3. Or supply fully custom weights — positive favours higher, negative penalises.
top = rank_index(
"./my-backtests/",
weights={
"sharpe_ratio": 3.0,
"sortino_ratio": 2.5,
"max_drawdown": -3.0,
"win_rate": 2.0,
"consistency_score": 1.5,
},
limit=25,
)
# 4. Materialise only the winners and render a focused dashboard.
store = LocalDirStore("./my-backtests/")
winners = [store.open(row["bundle_path"]) for row in top]
BacktestReport(backtests=winners).save("top25_risk_adjusted.html")Built-in focus presets:
| Preset | Prioritises |
|---|---|
BALANCED |
Equal mix of profit, risk-adjusted returns, drawdown penalties, and consistency |
PROFIT |
Absolute and relative gains (CAGR, net gain, win rate, profit factor) |
FREQUENCY |
High trade count, short durations, and per-trade efficiency |
RISK_ADJUSTED |
Sharpe, Sortino, Calmar with strong drawdown and volatility penalties |
Or from the shell:
iaf index ./my-backtests/
iaf rank ./my-backtests/ --by sharpe_ratio --where "summary_number_of_trades > 50" -n 20
iaf list ./my-backtests/ --sort calmar_ratio --json
iaf migrate-store --from local-dir --src ./my-backtests/ \
--to local-tiered --dst ./tiered/→ End-to-end runnable example: examples/storage_layer_demo/
Live Trading
Once a strategy proves itself in backtests, deploy it with the same code path you backtested. Connect to any exchange — use the built-in CCXT integration, or plug in your own OrderExecutor for brokers, FIX gateways, or any custom venue. Run locally, in Docker, or deploy serverless to AWS Lambda or Azure Functions. Built-in portfolio tracking, position management, order persistence, and automatic state recovery.
- No code rewrites — your
TradingStrategyruns identically in backtest, paper trading and live - Cloud deploy —
investing-algorithm-framework init --type aws_lambda/--type azure_function - Multiple exchanges & venues — CCXT integration out of the box (Binance, Bitvavo, Coinbase, Kraken …), or plug in your own
OrderExecutorfor any broker / FIX / custom venue - Portfolio persistence — trades, orders and positions survive restarts
Marketplace Integration
Publish your winning strategies to the Finterion marketplace and monetize them. Investors subscribe to your bot, you earn a recurring revenue share — the framework handles the technical integration.
Usage and Installation
To get started, install the framework and scaffold a new project:
pip install investing-algorithm-framework
# Generate project structure
investing-algorithm-framework init
# Or for cloud deployment
investing-algorithm-framework init --type aws_lambda
investing-algorithm-framework init --type azure_functionThe documentation provides guides and API reference. The quick start will walk you through your first strategy.
Creating a Strategy
The framework is designed around the TradingStrategy class. You define what data your strategy needs and when to buy or sell — the framework handles execution, position management, and reporting.
from typing import Dict, Any
import pandas as pd
from pyindicators import ema, rsi, crossover, crossunder
from investing_algorithm_framework import (
TradingStrategy, DataSource, TimeUnit, Schedule, DataType,
PositionSize, ScalingRule, StopLossRule, CooldownRule,
SignalSide, signals_from_column, signal_series_from_column,
)
class RSIEMACrossoverStrategy(TradingStrategy):
"""
EMA crossover + RSI filter strategy with position scaling and stop losses.
Buy when RSI is oversold AND a recent EMA crossover occurred.
Sell when RSI is overbought AND a recent EMA crossunder occurred.
Scale into winners, trail a stop loss, and let the framework handle the rest.
"""
schedule = Schedule.every(2, TimeUnit.HOUR)
symbols = ["BTC", "ETH"]
data_sources = [
DataSource(
identifier="BTC_ohlcv", symbol="BTC/EUR",
data_type=DataType.OHLCV, time_frame="2h",
market="BITVAVO", pandas=True, warmup_window=100,
),
DataSource(
identifier="ETH_ohlcv", symbol="ETH/EUR",
data_type=DataType.OHLCV, time_frame="2h",
market="BITVAVO", pandas=True, warmup_window=100,
),
]
# Risk management
position_sizes = [
PositionSize(symbol="BTC", percentage_of_portfolio=20),
PositionSize(symbol="ETH", percentage_of_portfolio=20),
]
scaling_rules = [
ScalingRule(
symbol="BTC", max_entries=3,
scale_in_percentage=[50, 25], cooldown_in_bars=5,
),
ScalingRule(
symbol="ETH", max_entries=3,
scale_in_percentage=[50, 25], cooldown_in_bars=5,
),
]
stop_losses = [
StopLossRule(
symbol="BTC", percentage_threshold=5,
sell_percentage=100, trailing=True,
),
StopLossRule(
symbol="ETH", percentage_threshold=5,
sell_percentage=100, trailing=True,
),
]
# Signal throttling: after a stop-out / sell, block re-entries on
# the same symbol for 12 bars, plus a portfolio-wide breather of
# 2 bars after any order to avoid same-bar pile-ups.
cooldowns = [
CooldownRule(
symbol="BTC", trigger="sell", blocks="buy", bars=12,
),
CooldownRule(
symbol="ETH", trigger="sell", blocks="buy", bars=12,
),
CooldownRule(trigger="any", blocks="any", bars=2),
]
def _add_signal_columns(self, df: pd.DataFrame) -> pd.DataFrame:
df = ema(df, period=12, source_column="Close",
result_column="ema_short")
df = ema(df, period=26, source_column="Close",
result_column="ema_long")
df = crossover(df, first_column="ema_short",
second_column="ema_long",
result_column="ema_crossover")
df = crossunder(df, first_column="ema_short",
second_column="ema_long",
result_column="ema_crossunder")
df = rsi(df, period=14, source_column="Close", result_column="rsi")
df["entry"] = (
(df["rsi"] < 30)
& (df["ema_crossover"].rolling(window=10).max() > 0)
).fillna(False)
df["exit"] = (
(df["rsi"] >= 70)
& (df["ema_crossunder"].rolling(window=10).max() > 0)
).fillna(False)
return df
def generate_signals(self, context, data: Dict[str, Any]):
"""Event-mode entry point — live, paper trading, event backtests."""
for symbol in self.symbols:
df = self._add_signal_columns(data[f"{symbol}_ohlcv"])
yield from signals_from_column(
df, "entry", side=SignalSide.OPEN_LONG, symbol=symbol,
source="rsi_ema_crossover",
)
yield from signals_from_column(
df, "exit", side=SignalSide.CLOSE_LONG, symbol=symbol,
source="rsi_ema_crossover",
)
def generate_signal_series(self, data: Dict[str, Any]):
"""Vector-mode entry point — only needed for vector backtests."""
for symbol in self.symbols:
df = self._add_signal_columns(data[f"{symbol}_ohlcv"])
yield signal_series_from_column(
df, "entry", side=SignalSide.OPEN_LONG, symbol=symbol,
source="rsi_ema_crossover",
)
yield signal_series_from_column(
df, "exit", side=SignalSide.CLOSE_LONG, symbol=symbol,
source="rsi_ema_crossover",
)Create as many strategy variants as you want — different parameters, different indicators, different symbols — then backtest them all and compare in a single report.
Backtest Report Dashboard
Every backtest produces a single HTML file you can open in any browser, share with teammates, or archive. No server, no dependencies, no Jupyter required.
from investing_algorithm_framework import BacktestReport
# After running backtests
report = BacktestReport(backtest)
report.show() # Opens dashboard in your browser
# Or load previously saved backtests from disk
report = BacktestReport.open(directory_path="path/to/backtests")
report.show()
# Compare multiple strategies side by side
report = BacktestReport.open(backtests=[backtest_a, backtest_b, backtest_c])
report.show()
# Save as a self-contained HTML file
report.save("my_report.html")Overview page — KPI cards, key metrics ranking table, trading activity, return scenarios, equity curves, metric bar charts, monthly returns heatmap, return distributions, and window coverage matrix.
Strategy pages — Deep dive into each strategy with per-run equity curves, rolling Sharpe, drawdown, monthly/yearly returns, and portfolio summary.
Capabilities
| Backtest Report Dashboard | Self-contained HTML report with ranking tables, equity curves, metric charts, heatmaps, and strategy comparison |
| Event-Driven Backtesting | Realistic, order-by-order simulation |
| Vectorized Backtesting | Fast signal research and prototyping |
| Cross-Sectional Pipelines | Compute factors across many symbols at once — rank, filter and score universes per iteration |
| 50+ Metrics | CAGR, Sharpe, Sortino, max drawdown, win rate, profit factor, recovery factor, volatility, and more |
| Live Trading | Connect to exchanges via CCXT for real-time execution |
| Portfolio Management | Position tracking, trade management, persistence |
| Cloud Deployment | Deploy to AWS Lambda, Azure Functions, or run as a web service |
| Market Data Providers | Built-in providers for CCXT, Yahoo Finance, Alpha Vantage, and Polygon — or build your own |
| Load External Data | Fetch CSV, JSON, or Parquet from any URL with caching, date parsing, and pre/post-processing |
| Record Custom Variables | Track any indicator or metric during backtests with context.record() |
| Strategies | OHLCV, tickers, custom data — Polars and Pandas native |
| Extensible | Custom data providers, order executors, and strategy classes |
Plugins
| Plugin | Description |
|---|---|
| PyIndicators | Technical analysis indicators (EMA, RSI, MACD, etc.) |
| Finterion Plugin | Share and monetize strategies on Finterion's marketplace |
We welcome contributions! Open an issue, pick one up, or send a PR.
git clone https://github.com/coding-kitties/investing-algorithm-framework.git
cd investing-algorithm-framework
poetry install
# Run all tests
python -m unittest discover -s tests- Open an issue for bugs or ideas
- Read the Contributing Guide
- Read the Architecture references
- PRs go against the
devbranch
- Documentation — Guides and API reference
- Quick Start — Get up and running
- Discord — Chat and support
- Reddit — Strategy discussion
If you use this framework for real trading, do not risk money you are afraid to lose. Test thoroughly with backtesting first. Start small. We assume no responsibility for your investment results.
We want to thank all contributors to this project. A full list can be found in AUTHORS.md.
Finterion — Marketplace for trading bots. Monetize your strategies by publishing them on Finterion.
