Build strategies, backtest at scale, compare results, and deploy the winner without rewriting your strategy.
Investing Algorithm Framework is a Python framework for the complete quantitative trading workflow. Define a strategy once, explore it with vector backtests, validate it in an event-driven simulation, inspect the results in an interactive dashboard, and run the same strategy in paper or live trading.
v9.0.0 alpha is available. Install the prerelease explicitly:
pip install investing-algorithm-framework==9.0.0a22See the v9.0 release notes and changelog. This README targets v9 and later; its APIs and examples may not work with earlier versions.
- 🧠 Advanced strategy definitions: Combine declarative indicators and data streams, confluence-scored entry and exit decisions, and position sizing and risk rules.
- 🔁 Long and short trading: Define entry and exit signals once and run the same strategy in vector backtests, event-driven backtests, paper trading, and live trading.
- ⚡ Vector backtesting: Evaluate signals and sweep thousands of strategy variants quickly with Polars-powered execution.
- 🏃 Event-driven backtesting: Validate candidates bar by bar with realistic orders, fills, costs, and portfolio management.
- 🪟 Studies, universes, and backtest windows: Define the assets, assumptions, and rolling, anchored, holdout, walk-forward, time-OOS, or universe-OOS periods for each experiment.
- 🗂️ Open Backtest Format: Keep complete vector and event results in portable, versioned
.obtfbundles. - 🗄️ Tiered storage and indexing: Rank and filter 10,000+ backtests through SQLite without decoding full result bundles.
- 📊 80+ performance metrics: Analyze CAGR, Sharpe, Sortino, Calmar, VaR/CVaR, drawdown, recovery, and benchmark-relative returns.
- 📈 Interactive dashboard: Compare strategies, inspect trades and charts, review window coverage, and save a self-contained HTML report.
- 🔀 Monte Carlo testing: Measure how often randomized market paths match or outperform a strategy's observed results.
- 🧮 Cross-sectional pipelines: Rank, filter, and score entire universes of symbols during each strategy iteration.
- 🧠 Confluence scoring cards: Build explainable decisions from requirements, weighted evidence, vetoes, and score thresholds.
- 🛡️ Declarative risk rules: Configure sizing, exposure limits, scaling, stop losses, take profits, and signal cooldowns.
- 💸 Execution cost models: Apply percentage, fixed, basis-point, or volume-aware commission and slippage assumptions.
- 🔐 Portfolio and credentials management: Configure markets, balances, fees, paper trading, and environment-based credentials without hardcoding secrets.
- 🚀 Flexible deployment: Run locally, self-host, deploy to AWS Lambda or Azure Functions, or publish through Finterion.
- 🌐 Extensible integrations: Add custom data providers, order executors, storage adapters, metrics, strategies, and optimizers.
- 🤖 Built-in MCP server: Let compatible AI tools query backtests, compare strategies, inspect trades, and manage research notes.
↔️ Position modes: NETTING vs. HEDGE: Choose one net direction per symbol or maintain independent long and short legs with separate risk controls and P&L.
Strategy Definition
There are three ways to define a strategy: use TradingStrategy for direct
orders or custom signals, or its ConfluenceScoringStrategy subclass for
declarative indicator-driven decisions. All declare their data and schedule;
choose how much of the trading logic you want to manage yourself:
Recommended: start with
ConfluenceScoringStrategy. We recommend it over the other two approaches because it:
- Provides both event and vector signal hooks, avoiding duplicated signal logic.
- Keeps indicator calculations, scoring rules, and thresholds declarative.
- Records explainable decision traces in event mode.
- Uses the shared sizing, risk, execution, and cooldown pipeline.
Use custom signals or direct orders as escape hatches when your strategy needs behavior that the declarative API cannot express.
| Style | Define | Execution modes |
|---|---|---|
| Direct orders | Override run_strategy and place orders through Context. |
Event backtests, paper, and live trading |
| Custom signals | Implement generate_signals and generate_signal_series; let the framework size and execute orders. |
Both backtest engines, paper, and live trading |
| Confluence scoring (recommended) | Subclass ConfluenceScoringStrategy and declare indicators and scored entry/exit cards. |
Both backtest engines, paper, and live trading |
No vector backtesting: this approach supports event-driven backtests, paper trading, and live trading only. Use custom signals or confluence cards below if you need vector backtesting.
Use run_strategy when you need direct control over order conditions, sizing,
and routing. This replaces the default signal-processing pipeline: declaring
strategy risk rules alone does not apply that pipeline to your custom orders.
from investing_algorithm_framework import (
DataSource, OrderSide, Schedule, TimeUnit, TradingStrategy,
)
class DirectOrderStrategy(TradingStrategy):
schedule = Schedule.every(2, TimeUnit.HOUR)
symbols = ["BTC"]
data_sources = [DataSource(
identifier="BTC_ohlcv", symbol="BTC/EUR", data_type="OHLCV", time_frame="2h",
market="BITVAVO", pandas=True, warmup_window=30,
)]
def run_strategy(self, context, data):
frame = data["BTC_ohlcv"]
price = float(frame["Close"].iloc[-1])
average = float(frame["Close"].rolling(20).mean().iloc[-1])
if price > average and not context.has_position("BTC"):
context.create_limit_order(
target_symbol="BTC", price=price, order_side=OrderSide.BUY,
percentage_of_portfolio=20,
)
elif price < average and context.has_position("BTC"):
context.create_limit_order(
target_symbol="BTC", price=price, order_side=OrderSide.SELL,
percentage_of_position=100,
)The vector engine does not call run_strategy. For vector backtesting, express
the strategy as signal series instead.
Emit trading intent rather than orders. The framework applies its normal conflict resolution, sizing, risk, execution, and cooldown phases.
from investing_algorithm_framework import (
DataSource, PositionSize, Schedule, SignalSide, TimeUnit, TradingStrategy,
signal_series_from_column, signals_from_column,
)
class MovingAverageSignalStrategy(TradingStrategy):
schedule = Schedule.every(2, TimeUnit.HOUR)
symbols = ["BTC"]
data_sources = [DataSource(
identifier="BTC_ohlcv", symbol="BTC/EUR", data_type="OHLCV", time_frame="2h",
market="BITVAVO", pandas=True, warmup_window=30,
)]
position_sizes = [PositionSize(symbol="BTC", percentage_of_portfolio=20)]
def _with_signals(self, data):
frame = data["BTC_ohlcv"].copy()
average = frame["Close"].rolling(20).mean()
frame["open_long"] = frame["Close"] > average
frame["close_long"] = frame["Close"] < average
return frame
def generate_signals(self, context, data):
frame = self._with_signals(data)
yield from signals_from_column(
frame, "open_long", side=SignalSide.OPEN_LONG, symbol="BTC",
)
yield from signals_from_column(
frame, "close_long", side=SignalSide.CLOSE_LONG, symbol="BTC",
)
def generate_signal_series(self, data):
frame = self._with_signals(data)
yield signal_series_from_column(
frame, "open_long", side=SignalSide.OPEN_LONG, symbol="BTC",
)
yield signal_series_from_column(
frame, "close_long", side=SignalSide.CLOSE_LONG, symbol="BTC",
)generate_signals evaluates the latest bar for event backtests, paper trading,
and live trading. generate_signal_series evaluates the full window for vector
backtests. Implement both to support both engines, sharing indicator logic to
keep their decisions consistent.
Inherit from ConfluenceScoringStrategy and declare indicator_configs plus
confluence_cards. The base class calculates the indicators and supplies both
signal hooks: no custom prepare_signal_data, generate_signals, or
generate_signal_series is needed.
This example uses pyindicators (pip install pyindicators). Pass its indicator
function as calculate, and declare the parameters and output columns explicitly.
Precomputation is appropriate here because RSI uses only present and past data.
from pyindicators import rsi
from investing_algorithm_framework import (
ConfluenceScoringStrategy, ConfluenceScoreConfig, CooldownRule,
DataSource, IndicatorConfig, IndicatorRule, PositionSize, Schedule,
SignalSide, StopLossRule, TakeProfitRule, TimeUnit,
)
class RSIConfluenceStrategy(ConfluenceScoringStrategy):
schedule = Schedule.every(2, TimeUnit.HOUR)
symbols = ["BTC"]
data_sources = [
DataSource(
identifier="BTC_ohlcv",
symbol="BTC/EUR",
data_type="OHLCV",
time_frame="2h",
market="BITVAVO",
pandas=True,
warmup_window=100,
)
]
indicator_configs = [
IndicatorConfig(
id="rsi",
calculate=rsi,
timeframe="2h",
data_source="BTC_ohlcv",
parameters={
"source_column": "Close", "period": 14, "result_column": "rsi",
},
output_columns=["rsi"],
minimum_history=15,
history="continuous",
execution="precompute",
),
]
confluence_cards = [
ConfluenceScoreConfig(
id=name,
side=side,
symbols=["BTC"],
rules={"rsi_threshold": IndicatorRule(
indicator_id="rsi",
output_column="rsi",
threshold=threshold,
comparison=comparison,
group_weights={"primary": 1},
lookback_period=1,
)},
group_aggregation={"primary": "sum"},
group_thresholds={"primary": 1},
)
for name, side, threshold, comparison in (
("long_entry", SignalSide.OPEN_LONG, 30, "le"),
("long_exit", SignalSide.CLOSE_LONG, 70, "ge"),
("short_entry", SignalSide.OPEN_SHORT, 70, "ge"),
("short_exit", SignalSide.CLOSE_SHORT, 30, "le"),
)
]
position_sizes = [PositionSize(symbol="BTC", percentage_of_portfolio=20)]
stop_losses = [
StopLossRule(
symbol="BTC",
percentage_threshold=5,
sell_percentage=100,
)
]
take_profits = [
TakeProfitRule(
symbol="BTC",
percentage_threshold=10,
sell_percentage=100,
)
]
cooldowns = [CooldownRule(trigger="any", blocks="any", bars=2)]The framework evaluates each card's explicit side and score threshold, then
routes qualifying signals through the same sizing, risk, execution, and cooldown
pipeline as custom signals. Event mode also records decision traces. The four
cards above cover long entry and exit plus short entry and cover.
See the declarative confluence strategy example for callable indicators, staged scale-ins, and execution-engine limitations.
Fees and slippage belong to the portfolio or backtest study because they describe a venue or scenario rather than signal logic.
See the strategy guide, the basic example, and the strategy showcase.
Backtesting
Backtests use a Study to describe the experiment and a
BacktestRunConfiguration to control execution and persistence.
from investing_algorithm_framework import BacktestRunConfiguration
results = app.run_backtests(
strategies=strategies,
study=training_study,
run_configuration=BacktestRunConfiguration(
backtest_storage_directory="./my-backtests",
n_workers=8,
memory_budget_mb=16_384,
min_available_memory_mb=4_096,
),
)
print(results.df)Both engines return a disk-backed BacktestIndex. Use the index for scalar
filtering and ranking, then load selected full results with
results.iter_backtests() or results.load_backtests().
The Polars-powered vector engine evaluates signal series in bulk. Use it for rapid signal research, parameter sweeps, large candidate sets, and early-stage filtering before running more expensive simulations.
The event engine processes market data bar by bar through the same strategy and order path used in live trading. Use it to validate execution behavior, fills, costs, portfolio changes, and interactions between strategies.
Study, Universe, and BacktestWindow support single-window tests, rolling
windows, anchored windows, holdouts, walk-forward k-fold validation, and
out-of-sample testing across time periods or universes. Execution assumptions
are captured with each study so results remain reproducible.
See the vector backtesting guide, the event backtesting guide, and the tutorial notebooks.
Backtest Storage
Each algorithm is stored as a versioned .obtf bundle using the
Open Backtest Format.
A bundle can contain studies, universes, windows, vector and event runs,
summaries, metrics, trades, orders, positions, snapshots, execution assumptions,
and Monte Carlo results.
The storage layer scales from local research to large result collections:
| Tier | Purpose |
|---|---|
| SQLite index | Rank and filter scalar metrics across 10,000+ results without decoding bundles |
BacktestStore |
Swap flat LocalDirStore and partitioned LocalTieredStore layouts |
| OHLCV chunks | Deduplicate content-addressed market data shared by multiple bundles |
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/See the storage layer example.
Deployment
The same strategy can run locally, in a container, as a web service, or in a serverless function. Portfolio state, orders, trades, and positions can persist across runs.
Connect to supported exchanges through CCXT or implement an OrderExecutor for
a broker, FIX gateway, or custom venue. Live mode evaluates schedules, loads
market data, creates orders, and maintains portfolio state continuously.
Paper trading exercises the live strategy path without sending real orders. Use it after event-driven validation to verify schedules, data feeds, credentials, and operational behavior in current market conditions.
Run the application on your own machine or infrastructure, package it in Docker, or scaffold AWS Lambda and Azure Functions projects from the CLI:
pip install investing-algorithm-framework
investing-algorithm-framework init
investing-algorithm-framework init --type aws_lambda
investing-algorithm-framework init --type azure_functionPublish validated strategies to the Finterion marketplace so investors can subscribe to them. The Finterion plugin handles the framework integration.
See the deployment guide and Finterion plugin.
Portfolio and Credentials Management
app.add_market() configures a portfolio and its market credentials together.
Set the trading currency, starting balance, fees, slippage, position mode, and
paper-trading behavior in one place:
app.add_market(
market="BITVAVO",
trading_symbol="EUR",
initial_balance=10_000,
fee_percentage=0.1,
paper_trading=True,
)Keep secrets outside source code with market-scoped environment variables:
BITVAVO_API_KEY=<your-api-key>
BITVAVO_SECRET_KEY=<your-api-secret>Explicit credentials from a secret manager can be registered with
MarketCredential. Deployment-level BITVAVO_OVERRIDE_* variables can enforce
credentials, paper mode, and managed balance regardless of values supplied by
the application.
Portfolio state, orders, positions, and trades persist across runs. Optional portfolio synchronization reconciles broker balances and supports recurring or one-off deposit schedules. Local paper trading requires no real credentials.
See Portfolio Configuration, Credential Management, and Portfolio Synchronization.
Dashboard
BacktestReport creates a self-contained interactive HTML dashboard. Compare
strategies, inspect equity and drawdown curves, review trades, analyze monthly
and yearly returns, check window coverage, and add research notes without
running a separate server.
from investing_algorithm_framework import BacktestReport
# Materialize only the selected results when working with a large index.
BacktestReport(
backtests=results.load_backtests(workers=1),
).save("backtest-report.html")
# Or reopen a directory of persisted bundles later.
BacktestReport.open(
directory_path="./my-backtests/",
workers=-1,
show_progress=True,
).save("backtest-report.html")The built-in MCP server lets compatible AI tools query stored backtests, compare
strategies, inspect trades, and work with report notes through
investing-algorithm-framework mcp.
See the report guide and MCP server guide.
Advanced Features
- Backtest optimization: plug in an ask/tell
StrategyOptimizer, budget candidate evaluations, and resume saved search state. - Long and short trading: use open/close signals for either side with fill-based P&L and collateral handling.
- Cross-sectional pipelines: rank, filter, and score a universe of symbols on each iteration.
- Confluence cards: combine primary conditions, weighted evidence, requirements, vetoes, and score thresholds.
- Decision traces: retain indicator values, rule outcomes, and scores for signals and no-op decisions.
- Risk rules: configure position sizing, exposure limits, scaling, stop losses, take profits, and signal cooldowns.
- Execution models: use percentage, fixed, basis-point, or volume-aware commission and slippage models.
- Monte Carlo testing: estimate whether performance could plausibly occur by chance.
- Metrics and benchmarks: analyze CAGR, Sharpe, Sortino, Calmar, VaR/CVaR, drawdown, recovery, and benchmark-relative performance.
- External data and custom variables: load cached CSV, JSON, or Parquet data and record strategy-specific values.
- Portfolio synchronization: model recurring or one-off deposits and reconcile live balances with a broker.
- Bounded execution: control workers, memory admission, checkpoints, progress, and failure handling for large sweeps.
See the advanced concepts documentation and backtest optimization guide.
Position Modes: NETTING vs. HEDGE
Portfolios use PositionMode.NETTING by default: each symbol has one net
direction, so a long and short cannot coexist. Enable PositionMode.HEDGE to
maintain independent long and short legs for the same symbol, each with its own
entry, scaling, stop-loss, take-profit, cooldown, and P&L.
from investing_algorithm_framework import PositionMode
app.add_market(
market="BITVAVO",
trading_symbol="EUR",
initial_balance=10_000,
position_mode=PositionMode.HEDGE,
)Both backtest engines support OPEN_LONG, CLOSE_LONG, OPEN_SHORT, and
CLOSE_SHORT independently in HEDGE mode. In NETTING mode,
flip_on_opposite_signal=True can close the current direction and open the
opposite direction on the same bar.
Live HEDGE trading requires an OrderExecutor and PortfolioProvider that
explicitly support it. The built-in CCXT adapters support NETTING only, so use
custom HEDGE-capable adapters for live execution.
Pipelines
Pipelines compute factors across many symbols in one pass and return a tidy cross-sectional table on each strategy iteration. Use them to define a tradeable universe, rank candidates, normalize factors, and construct multi-factor or risk-neutral signals without manually looping over symbols.
from investing_algorithm_framework import AverageDollarVolume, Pipeline, Returns
class MomentumScreener(Pipeline):
dollar_volume = AverageDollarVolume(window=30)
momentum = Returns(window=30)
universe = dollar_volume.top(100)
alpha = momentum.rank(mask=universe)
class MomentumStrategy(TradingStrategy):
pipelines = [MomentumScreener]
def generate_signals(self, context, data):
candidates = data["MomentumScreener"]
leaders = candidates.sort("alpha", descending=True).head(10)
# Yield Signal objects for the selected symbols.
...Built-in factors include returns, liquidity, moving averages, RSI, volatility, cross-sectional means, rolling beta, and neutralization. Factors compose with arithmetic, ranking, filtering, z-scoring, demeaning, winsorization, and grouped transforms.
See the Pipelines guide, event backtest integration, vector backtest integration, and live integration.
Monte Carlo Testing
Monte Carlo permutation testing helps distinguish a strategy's observed edge from results that randomized market paths could produce by chance. The framework runs the strategy on its original OHLCV data, creates randomized permutations, reruns the same strategy on each permutation, and compares the real metrics with the resulting null distributions.
Use app.run_monte_carlo_test(...) with a strategy, a BacktestDateRange, and
the desired number of permutations. The returned BacktestMonteCarloTest
contains the real metrics, metrics from every permuted run, the original and
permuted datasets, and p-values for metrics including:
- CAGR, Sharpe, Sortino, and Calmar ratios
- Profit factor, win rate, and win/loss ratio
- Annual volatility and maximum drawdown
- Average monthly return
Lower p-values indicate that fewer randomized runs matched or exceeded the observed result. They are evidence about statistical significance, not proof of future profitability. Use enough permutations for the precision you need and combine the result with out-of-sample and walk-forward validation.
See the tutorial notebooks for the robustness analysis workflow.
Plugins and Supported Libraries
| Integration | Purpose |
|---|---|
| PyIndicators | Technical indicators including EMA, RSI, and MACD |
| Finterion plugin | Publish and operate strategies on Finterion |
| CCXT | Exchange market data and live order execution |
| Pandas and Polars | Native tabular inputs for strategy and backtest workflows |
| Yahoo Finance, Alpha Vantage, and Polygon | Built-in market data providers |
| AWS and Azure | Optional state storage and serverless deployment integrations |
Custom data providers, order executors, storage adapters, strategies, metrics, and optimizers can be added without replacing the core workflow.
Contributions are welcome. Open an issue, choose an existing one, or submit a
pull request against the dev branch.
git clone https://github.com/coding-kitties/investing-algorithm-framework.git
cd investing-algorithm-framework
poetry install
python -m unittest discover -s testsDo not risk money you cannot afford to lose. Backtests and paper trading cannot guarantee future performance. Validate strategy behavior, execution assumptions, stored results, and operational safeguards before trading with real funds. The project and its contributors assume no responsibility for investment results.
Thank you to everyone who has contributed code, documentation, testing, ideas, and feedback. See AUTHORS.md for the contributor list.
Finterion is a marketplace for trading bots where strategy creators can publish and monetize their work.