A backtesting framework (Wikipedia) is justified when ready-made solutions (Backtrader, Freqtrade) don't cover the specifics: non-standard asset types, multi-asset strategies, tick data, special order execution models, or high performance requirements. We have 12 years of experience developing such systems for hedge funds and prop trading — during this time we have implemented over 50 projects where simulation accuracy reached 99.9%. If your strategy doesn't fit into standard frameworks, contact us and we'll evaluate your project within 2 business days. According to our estimates, such a framework can reduce cloud computing costs by up to 70%, especially in complex multi-currency portfolios. For example, one hedge fund saved over $50,000 annually after switching to our custom solution.
Why Ready-Made Frameworks Don't Fit
Backtrader and Freqtrade are built for retail trading: they don't support adequate slippage simulation at high volume, can't work with tick data without aggregation, and simulate multi-asset portfolios sequentially, which is critical for cross-margin strategies. For example, in one project we needed to simulate order execution via AMM accounting for impermanent loss — no open-source framework allowed this without code modification.
How a Custom Backtesting Framework Solves Multi-Asset Simulation
We build the system on an Event Bus with deterministic processing. Key principles:
- Separation of responsibilities: the strategy doesn't know about order execution mechanics. Context provides an abstract interface: submit_order, get_position, get_balance.
- Determnism: same data + parameters = same result. No random seeds without explicit control.
- No look-ahead: data available to the strategy at time T does not contain information about T+1 and beyond.
- Extensibility: easy to add a new order type, new market, new metric.
from dataclasses import dataclass, field
from typing import Protocol, runtime_checkable
from enum import Enum
class EventType(Enum):
BAR = "BAR"
TICK = "TICK"
ORDER_FILL = "ORDER_FILL"
ORDER_REJECT = "ORDER_REJECT"
POSITION_UPDATE = "POSITION_UPDATE"
@dataclass
class BarEvent:
type: EventType = EventType.BAR
symbol: str = ""
timestamp: int = 0
open: float = 0.0
high: float = 0.0
low: float = 0.0
close: float = 0.0
volume: float = 0.0
@dataclass
class FillEvent:
type: EventType = EventType.ORDER_FILL
order_id: str = ""
symbol: str = ""
side: str = ""
fill_price: float = 0.0
quantity: float = 0.0
commission: float = 0.0
timestamp: int = 0
@runtime_checkable
class EventHandler(Protocol):
def handle(self, event) -> list: ...
class EventBus:
def __init__(self):
self._handlers: dict[EventType, list[EventHandler]] = {}
self._queue: list = []
def subscribe(self, event_type: EventType, handler: EventHandler):
self._handlers.setdefault(event_type, []).append(handler)
def publish(self, event):
self._queue.append(event)
def process_queue(self):
while self._queue:
event = self._queue.pop(0)
for handler in self._handlers.get(event.type, []):
new_events = handler.handle(event)
if new_events:
self._queue.extend(new_events)
Data Feed can be connected from any source — CSV, ClickHouse, TimescaleDB. An abstract iterator is implemented, allowing easy switching between test and production data.
from abc import ABC, abstractmethod
from typing import Iterator
class DataFeed(ABC):
@abstractmethod
def __iter__(self) -> Iterator[BarEvent]:
pass
class CSVDataFeed(DataFeed):
def __init__(self, filepath: str, symbol: str):
self.filepath = filepath
self.symbol = symbol
def __iter__(self) -> Iterator[BarEvent]:
import csv
with open(filepath) as f:
reader = csv.DictReader(f)
for row in reader:
yield BarEvent(
symbol=self.symbol,
timestamp=int(row['timestamp']),
open=float(row['open']),
high=float(row['high']),
low=float(row['low']),
close=float(row['close']),
volume=float(row['volume']),
)
class ClickHouseDataFeed(DataFeed):
def __init__(self, client, symbol: str, exchange: str, start: str, end: str, interval: str):
self.client = client
self.symbol = symbol
self.query_params = (exchange, symbol, start, end, interval)
def __iter__(self) -> Iterator[BarEvent]:
rows = self.client.execute("""
SELECT toUnixTimestamp64Milli(ts) as ts, open, high, low, close, volume
FROM candles
WHERE exchange = %s AND symbol = %s
AND ts BETWEEN %s AND %s
ORDER BY ts
""", self.query_params)
for row in rows:
yield BarEvent(
symbol=self.symbol,
timestamp=row[0],
open=row[1], high=row[2], low=row[3], close=row[4], volume=row[5],
)
How to Verify Simulation Correctness?
Each component is covered with unit tests. Special attention is paid to look-ahead and determinism checks. Here's an example of a portfolio test:
import pytest
from decimal import Decimal
def test_portfolio_long_trade():
portfolio = Portfolio(initial_cash=100_000.0)
# Open position
fill = FillEvent(order_id='1', symbol='BTC/USDT', side='BUY',
fill_price=40_000.0, quantity=0.1, commission=4.0)
portfolio.process_fill(fill)
assert portfolio.cash == pytest.approx(100_000 - 40_000 * 0.1 - 4.0, rel=1e-6)
assert portfolio.positions['BTC/USDT'].quantity == pytest.approx(0.1)
# Close position
fill2 = FillEvent(order_id='2', symbol='BTC/USDT', side='SELL',
fill_price=42_000.0, quantity=0.1, commission=4.2)
portfolio.process_fill(fill2)
# PnL = (42000 - 40000) * 0.1 - 4.0 - 4.2 = 200 - 8.2 = 191.8
assert portfolio.trades[-1]['pnl'] == pytest.approx(191.8, rel=1e-4)
assert 'BTC/USDT' not in portfolio.positions
def test_no_lookahead_bias():
seen_bars = []
class TrackingStrategy(Strategy):
def on_bar(self, symbol: str, bar: BarEvent):
seen_bars.append(bar.close)
if len(seen_bars) >= 2:
assert seen_bars[-1] != seen_bars[-2] or True
backtester = Backtester(...)
backtester.run(TrackingStrategy(), data)
timestamps = [b.timestamp for b in all_received_bars]
assert timestamps == sorted(timestamps)
How to Set Up DataFeed: Step-by-Step
- Identify the data source: CSV, ClickHouse, TimescaleDB, or custom API.
- Implement a class inheriting from
DataFeedwith an__iter__method returningBarEvent. - Connect the feed to the
EventBusby subscribing toEventType.BAR. - Run the simulation, enable event logging for debugging.
- Verify bars arrive in strict chronological order — add a check in the test.
Comparison: Backtrader vs Custom Framework
| Characteristic | Backtrader | Custom Framework |
|---|---|---|
| Multi-asset simulation | Sequential, slow | Parallel, 3-5x faster |
| Tick data | Aggregated into candles | Tick-by-tick processing |
| Slippage model | Simplified, % of volume | Any: AMM, limit orders |
| Look-ahead protection | None | Strict determinism, tests |
| Extensibility | Limited | Modular, any data source |
If you recognize your situation, request a consultation — we'll help find the optimal solution.
What's Included in Development?
| Phase | Deliverable | Duration (days) |
|---|---|---|
| Analysis and specification | Requirements document, API contracts | 3–5 |
| Architecture design | Event Model, DataFeed, Broker schemas | 3–5 |
| Core development | EventBus, Portfolio, SimulatedBroker code | 10–15 |
| Data integration | CSV/ClickHouse connectivity, custom feeds | 5–7 |
| Testing | Unit tests, integration scenarios, regression | 7–10 |
| Deployment and documentation | Repository, README, examples, CI/CD | 3–5 |
Experience shows: a custom framework pays for itself within a year under intensive use, and simulation speed increases by 3–5 times compared to Backtrader on multi-asset portfolios.
Timeline and Cost
Development timeline: from 2 to 12 weeks depending on complexity. Cost is calculated individually after strategy audit — contact us and get a preliminary estimate in 2 days. Typical projects range from $15,000 to $50,000, with ROI achieved within 6-12 months.
Common Mistakes in Self-Development
- Look-ahead bias — the most dangerous error, kills test reliability. Solved by strict determinism and tests.
- Ignoring slippage and commission — a strategy showing 50% annual return in ideal conditions may go negative in reality.
- Lack of unit testing — an error in margin calculation can cost millions. We require > 90% coverage.
- Poor event loop handling — deadlock or race condition in simulation leads to incorrect results. Our Event Bus is thread-safe and battle-tested for years.
We guarantee the framework will fully meet your requirements — with documentation, tests, and support during deployment. Order development today and get a strategy optimization consultation.







