Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ Backtesting uses the same event model as live trading.

High-level flow:

- CSV or historical provider feeds market data
- Backtest runner drives event loop
- Strategy executes normally
- Dukascopy cache data is loaded via `BacktestClient.from_dukascopy_cache(...)`
- `run_backtest(...)` drives the event loop and recording pipeline
- Strategy code executes unchanged
- Portfolio and recording operate identically to live mode

See `docs/backtesting_guide.md` for detailed usage.
See `docs/backtesting_guide.md` for the current cache-backed workflow.


## Live Trading (IG)
Expand Down Expand Up @@ -116,7 +116,19 @@ Users can subscribe to recording events for custom reporting pipelines.

## Installation

Install using your preferred environment manager. Ensure Python 3.11+.
Python 3.11+ is required.

Install the published package:

```bash
pip install tradedesk
```

For local development:

```bash
pip install -e '.[dev]'
```


## Documentation
Expand All @@ -131,6 +143,15 @@ See the `docs/` directory for:
- Risk management guide
- Metrics guide

Public package entry points are grouped under:

- `tradedesk.marketdata`
- `tradedesk.execution`
- `tradedesk.execution.backtest`
- `tradedesk.portfolio`
- `tradedesk.recording`
- `tradedesk.strategy`


tradedesk is designed for clarity, determinism, and event-level
transparency.
Expand All @@ -142,4 +163,3 @@ Licensed under the Apache License, Version 2.0.
See: [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)

Copyright 2026 [Radius Red Ltd.](https://github.com/radiusred)

8 changes: 4 additions & 4 deletions docs/aggregation_guide.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Candle Aggregation Guide

The `tradedesk.aggregation` module provides time-bucketing candle aggregation for converting base-period candles into higher timeframes.
The `tradedesk.marketdata` module provides time-bucketing candle aggregation for converting base-period candles into higher timeframes.

## Overview

Expand All @@ -14,7 +14,7 @@ The `tradedesk.aggregation` module provides time-bucketing candle aggregation fo
## Basic Usage

```python
from tradedesk.aggregation import CandleAggregator
from tradedesk.marketdata import CandleAggregator
from tradedesk import Candle

# Create aggregator for 15-minute candles from 5-minute base period
Expand Down Expand Up @@ -49,7 +49,7 @@ assert result.close == 1.125 # Last close
Use `choose_base_period()` to automatically select an appropriate base period for your broker:

```python
from tradedesk.aggregation import choose_base_period
from tradedesk.marketdata import choose_base_period

# Default: Uses common broker periods (SECOND, 1MINUTE, 5MINUTE, HOUR)
base = choose_base_period("15MINUTE") # Returns "1MINUTE"
Expand Down Expand Up @@ -157,7 +157,7 @@ The aggregator is gap-tolerant:
## Complete Example: Live Aggregation

```python
from tradedesk.aggregation import CandleAggregator
from tradedesk.marketdata import CandleAggregator

class LiveAggregationStrategy:
def __init__(self, target_period: str):
Expand Down
57 changes: 42 additions & 15 deletions docs/backtesting_guide.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Backtesting Guide

This guide covers running backtests with tradedesk using candle data loaded from
CSV files or supplied in-memory.
the Dukascopy cache or supplied in-memory.

By the end you will have:

- A working strategy
- A CSV-driven backtest via `run_portfolio`
- A cache-backed backtest via `run_portfolio`
- A full recorded backtest via `run_backtest` (with metrics and trade output)

The same strategy runs live against a broker without modification.
Expand All @@ -19,21 +19,30 @@ The same strategy runs live against a broker without modification.
my_backtest/
strategy.py
run_backtest.py
candles.csv
```

---

## 2. Candle CSV Format
## 2. Dukascopy Cache Input

```
timestamp,open,high,low,close,volume
2025-01-01T00:00:00Z,1.2500,1.2510,1.2490,1.2505,1000
2025-01-01T00:05:00Z,1.2505,1.2520,1.2500,1.2515,800
```
`BacktestClient.from_dukascopy_cache(...)` reads 1-minute candle files from a
Dukascopy cache directory and aggregates them to the period your strategy
subscribes to.

Required inputs:

- `cache_dir`: root cache directory
- `symbol`: cache symbol folder, for example `GBPUSD`
- `instrument`: instrument identifier used by your strategy
- `period`: target tradedesk period such as `5MINUTE` or `HOUR`
- `date_from` / `date_to`: inclusive date range
- `price_side`: `"bid"` or `"ask"` (`"bid"` is the default)

Example shared cache location used at Radius Red:

Fields `volume` and `tick_count` are optional.
Timestamps may use `-` or `/` as date separators, with or without a trailing `Z`.
```text
/paperclip/tradedesk/marketdata/GBPUSD/2026/00/01_bid.csv.zst
```

---

Expand Down Expand Up @@ -82,17 +91,23 @@ code path as live trading.

```python
# run_backtest.py
from datetime import date

from tradedesk import SimplePortfolio, run_portfolio
from tradedesk.execution.backtest.client import BacktestClient

from strategy import SimpleMomentumStrategy


def client_factory():
return BacktestClient.from_csv(
"candles.csv",
return BacktestClient.from_dukascopy_cache(
"/paperclip/tradedesk/marketdata",
symbol="GBPUSD",
instrument="CS.D.GBPUSD.TODAY.IP",
period="5MINUTE",
date_from=date(2025, 1, 1),
date_to=date(2025, 1, 31),
price_side="bid",
)


Expand All @@ -112,7 +127,14 @@ To capture the client for inspection, use a closure:
created = {}

def client_factory():
c = BacktestClient.from_csv("candles.csv", instrument="CS.D.GBPUSD.TODAY.IP", period="5MINUTE")
c = BacktestClient.from_dukascopy_cache(
"/paperclip/tradedesk/marketdata",
symbol="GBPUSD",
instrument="CS.D.GBPUSD.TODAY.IP",
period="5MINUTE",
date_from=date(2025, 1, 1),
date_to=date(2025, 1, 31),
)
created["client"] = c
return c

Expand Down Expand Up @@ -169,6 +191,7 @@ run_portfolio(

```python
import asyncio
from datetime import date
from pathlib import Path

from tradedesk import SimplePortfolio
Expand All @@ -181,7 +204,11 @@ async def main():
spec = BacktestSpec(
instrument="CS.D.GBPUSD.TODAY.IP",
period="5MINUTE",
candle_csv=Path("candles.csv"),
cache_dir=Path("/paperclip/tradedesk/marketdata"),
symbol="GBPUSD",
date_from=date(2025, 1, 1),
date_to=date(2025, 1, 31),
price_side="bid",
half_spread_adjustment=0.5, # add half the spread to BID-sourced candles
)

Expand Down
14 changes: 7 additions & 7 deletions docs/metrics_guide.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Performance Metrics Guide

The `tradedesk.metrics` module provides tools for analyzing trading strategy performance.
The `tradedesk.recording` module provides tools for analyzing trading strategy performance.

## Overview

Expand All @@ -13,7 +13,7 @@ The metrics module helps you:
## Quick Start

```python
from tradedesk.metrics import compute_metrics
from tradedesk.recording import compute_metrics

# Your trade fills
trade_rows = [
Expand Down Expand Up @@ -44,7 +44,7 @@ print(f"Max Drawdown: {metrics.max_drawdown:.2f}")
Convert fill history into complete round-trip trades:

```python
from tradedesk.metrics import round_trips_from_fills
from tradedesk.recording import round_trips_from_fills

fills = [
{"instrument": "EURUSD", "direction": "BUY", "timestamp": "2025-01-01T10:00:00Z", "price": "1.1000", "size": "2"},
Expand Down Expand Up @@ -86,7 +86,7 @@ trips = round_trips_from_fills(fills)
The `Metrics` dataclass contains comprehensive performance statistics:

```python
from tradedesk.metrics import Metrics, compute_metrics
from tradedesk.recording import Metrics, compute_metrics

metrics = compute_metrics(equity_rows, trade_rows)

Expand Down Expand Up @@ -157,7 +157,7 @@ assert metrics.final_equity == 0.0
Build an equity curve from round-trip P&L:

```python
from tradedesk.metrics import round_trips_from_fills, equity_rows_from_round_trips
from tradedesk.recording import round_trips_from_fills, equity_rows_from_round_trips

fills = [
{"instrument": "EURUSD", "direction": "BUY", "timestamp": "2025-01-01T00:00:00Z", "price": "100", "size": "1"},
Expand Down Expand Up @@ -216,7 +216,7 @@ print(f"Avg win: ${metrics_usd.avg_win}")
Calculate maximum drawdown from an equity curve:

```python
from tradedesk.metrics import max_drawdown
from tradedesk.recording.metrics import max_drawdown

equity = [100, 110, 105, 95, 120, 115]
dd = max_drawdown(equity) # -15.0 (peak 110, trough 95)
Expand All @@ -229,7 +229,7 @@ assert max_drawdown([100, 101, 102]) == 0.0 # Monotonic up
## Complete Example: Strategy Analysis

```python
from tradedesk.metrics import compute_metrics, round_trips_from_fills
from tradedesk.recording import compute_metrics, round_trips_from_fills

class StrategyAnalyzer:
def __init__(self):
Expand Down
10 changes: 5 additions & 5 deletions docs/risk_management.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Risk Management Guide

The `tradedesk.risk` and `tradedesk.position` modules provide utilities for position sizing and position state tracking.
The `tradedesk.portfolio` and `tradedesk.execution` modules provide utilities for position sizing and position state tracking.

## Position Sizing

Expand All @@ -9,7 +9,7 @@ The `tradedesk.risk` and `tradedesk.position` modules provide utilities for posi
The `atr_normalised_size()` function calculates position size based on Average True Range (ATR):

```python
from tradedesk.risk import atr_normalised_size
from tradedesk.portfolio import atr_normalised_size

# Calculate position size
size = atr_normalised_size(
Expand Down Expand Up @@ -61,7 +61,7 @@ size = atr_normalised_size(

```python
# Adjust position size based on market volatility
from tradedesk.indicators import ATR
from tradedesk.marketdata.indicators import ATR

atr_indicator = ATR(period=14)

Expand All @@ -86,7 +86,7 @@ if current_atr:
The `PositionTracker` class maintains state for an open position:

```python
from tradedesk.position import PositionTracker
from tradedesk.execution import PositionTracker
from tradedesk.types import Direction

# Create tracker
Expand Down Expand Up @@ -155,7 +155,7 @@ assert position.mfe_points == 7.0 # 107 - 100 (updated to new max)
### Complete Position Lifecycle

```python
from tradedesk.position import PositionTracker
from tradedesk.execution import PositionTracker
from tradedesk.types import Direction

class MyStrategy:
Expand Down
3 changes: 1 addition & 2 deletions docs/strategy_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,7 @@ The implementation uses two cooperating components:
```python
from dataclasses import dataclass

from tradedesk.indicators.ema import EMA
from tradedesk.indicators.atr import ATR
from tradedesk.marketdata.indicators import ATR, EMA


@dataclass
Expand Down
15 changes: 11 additions & 4 deletions tests/portfolio/test_crash_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@

from tradedesk import Direction
from tradedesk.execution import BrokerPosition, PositionTracker
from tradedesk.portfolio import Instrument, JournalEntry, PositionJournal, ReconciliationManager
from tradedesk.portfolio import (
Instrument,
JournalEntry,
PositionJournal,
ReconciliationManager,
SleeveId,
)
from tradedesk.types import Candle

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -61,6 +67,7 @@ class _FakeStrategy:

def __init__(self, epic: str = "") -> None:
self.epic = epic
self.instrument = Instrument(epic)
self.position = PositionTracker()
self.entry_atr: float = 0.0
self._on_position_change: Callable[[str], None] | None = None
Expand Down Expand Up @@ -101,7 +108,7 @@ def _build_manager(
) -> ReconciliationManager:
if client is None:
client = AsyncMock()
strategies = {Instrument(e): _FakeStrategy(e) for e in epics}
strategies = {SleeveId(e): _FakeStrategy(e) for e in epics}
runner = MagicMock()
runner.strategies = strategies
mgr = ReconciliationManager(
Expand All @@ -111,13 +118,13 @@ def _build_manager(
target_period="HOUR",
enable_event_subscription=False,
)
for inst, strat in strategies.items():
for strat in strategies.values():
strat._on_position_change = mgr.persist_positions
return mgr


def _strat(mgr: ReconciliationManager, epic: str) -> _FakeStrategy:
return mgr._runner.strategies[Instrument(epic)] # type: ignore[return-value]
return mgr._runner.strategies[SleeveId(epic)] # type: ignore[return-value]


@pytest.fixture
Expand Down
Loading
Loading