Skip to content

Commit 66cc063

Browse files
committed
chore: v8.8.0 — re-exports, DI wiring, examples for new features
Bumps version 8.7.3 -> 8.8.0. Wires new BrokerBalanceTracker into the dependency container and event loop, exposes new public symbols from package __init__, adds OperationalException for sync errors, updates docs sidebar, and ships example strategies showcasing TWR metrics, DCA accumulation, and stop/stop-limit orders.
1 parent d2b6303 commit 66cc063

91 files changed

Lines changed: 4869 additions & 10 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ This framework is built around the full loop: **create strategies → vector bac
8585
- 📄 **[One-Click HTML Report](https://coding-kitties.github.io/investing-algorithm-framework/Getting%20Started/backtest-reports)** — Self-contained file, no server, dark & light theme, shareable
8686
- 📦 **[Custom `.iafbt` Backtest Bundle Format](https://coding-kitties.github.io/investing-algorithm-framework/Data/backtest_data)** — An explicit, versioned, compressed, language-portable container (zstd + msgpack with magic-byte header) plus a separate parquet index for fast filtering without loading. ~21× smaller and ~27× fewer files than standard filebased directory layouts, with parallel I/O for fast load/save of large amounts of backtests.
8787
- 🌐 **[Load External Data](https://coding-kitties.github.io/investing-algorithm-framework/Data/external-data)** — Fetch CSV, JSON, or Parquet from any URL with caching and auto-refresh
88-
- 📝 **[Record Custom Variables](https://coding-kitties.github.io/investing-algorithm-framework/Advanced%20Concepts/recording-variables)** — Track any indicator or metric during backtests with `context.record()`
88+
-**[Per-Market Deposit Schedules & Portfolio Sync](https://coding-kitties.github.io/investing-algorithm-framework/Advanced%20Concepts/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 — same `context.sync_portfolio()` API in both modes.
89+
- �📝 **[Record Custom Variables](https://coding-kitties.github.io/investing-algorithm-framework/Advanced%20Concepts/recording-variables)** — Track any indicator or metric during backtests with `context.record()`
8990
- 🚀 **[Build → Backtest → Deploy](https://coding-kitties.github.io/investing-algorithm-framework/Getting%20Started/application-setup)** — Local dev, cloud deploy (AWS / Azure), or monetize on Finterion
9091

9192
</details>

docusaurus/sidebars.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ const sidebars = {
133133
type: 'doc',
134134
id: 'Advanced Concepts/recording-variables',
135135
},
136+
{
137+
type: 'doc',
138+
id: 'Advanced Concepts/portfolio-sync',
139+
},
136140
{
137141
type: 'doc',
138142
id: 'Advanced Concepts/pipelines',
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Pipeline API examples
2+
3+
This folder showcases the **Pipeline API (Phase 1)** — a declarative way to
4+
express cross-sectional screens and signals over a panel of symbols, similar
5+
in spirit to Quantopian / Zipline pipelines.
6+
7+
A `Pipeline` is a small class where you declare the columns you want as
8+
`Factor` / `Filter` attributes. The engine turns those declarations into a
9+
single Polars DataFrame on each scheduled run, with no look-ahead, ready for
10+
your `TradingStrategy` to consume via `data["YourPipelineClassName"]`.
11+
12+
## Examples in this folder
13+
14+
| File | What it shows |
15+
| --- | --- |
16+
| [`momentum_screener.py`](momentum_screener.py) | Minimal screener-only example: a `MomentumScreener` ranks 5 EUR pairs by 30‑day return within the top‑3 most liquid names and prints the top‑2 each day. No order placement — useful for understanding the pipeline output format. |
17+
| [`cross_sectional_momentum_bot.py`](cross_sectional_momentum_bot.py) | End-to-end trading bot: a `MomentumScreener` ranks symbols by 30‑day return within the top‑3 most liquid names, and a `TradingStrategy` rebalances daily into the top‑2 ranked symbols on Bitvavo. Includes a 1‑year backtest. |
18+
19+
## Anatomy of the example
20+
21+
```python
22+
class MomentumScreener(Pipeline):
23+
dollar_volume = AverageDollarVolume(window=30)
24+
momentum = Returns(window=30)
25+
universe = dollar_volume.top(3) # liquidity filter
26+
alpha = momentum.rank(mask=universe) # ranked score
27+
```
28+
29+
That's the whole pipeline. The strategy then consumes it:
30+
31+
```python
32+
class CrossSectionalMomentumBot(TradingStrategy):
33+
pipelines = [MomentumScreener]
34+
...
35+
36+
def run_strategy(self, context, data):
37+
screen = data["MomentumScreener"] # polars.DataFrame
38+
targets = screen.sort("alpha", descending=True).head(TOP_N)
39+
# ... rebalance into `targets` ...
40+
```
41+
42+
## Run a backtest
43+
44+
From the repository root:
45+
46+
```bash
47+
python examples/cross-sectional-pipelines/cross_sectional_momentum_bot.py
48+
```
49+
50+
The script:
51+
52+
1. Creates an app pointed at `examples/resources/`.
53+
2. Registers OHLCV data sources for 7 EUR pairs on Bitvavo.
54+
3. Runs a 1‑year daily backtest of the momentum rotation.
55+
4. Prints the full `Backtest` JSON (metrics, trades, snapshots).
56+
57+
For an interactive HTML dashboard, replace the `print(backtest)` line with:
58+
59+
```python
60+
from investing_algorithm_framework import BacktestReport
61+
BacktestReport(backtest).show()
62+
```
63+
64+
## Run it live
65+
66+
Remove the `app.run_backtest(...)` block at the bottom of the file and call
67+
`app.run()` instead. Bitvavo does not require API keys for market data, so
68+
the bot will pull live OHLCV out of the box. Add Bitvavo credentials via the
69+
standard config to enable live order execution.
70+
71+
## Built-in factors / filters used
72+
73+
- `AverageDollarVolume(window=N)` — rolling close × volume over N bars.
74+
- `Returns(window=N)` — N‑bar percentage change.
75+
- `Factor.rank(mask=...)` — cross‑sectional rank, optionally restricted to a filter.
76+
- `Factor.top(n)` / `Factor.bottom(n)` — per‑bar top/bottom‑N selection (returns a `Filter`).
77+
78+
See the framework docs:
79+
80+
- `docusaurus/docs/Advanced Concepts/pipelines.md`
81+
- `docusaurus/docs/Advanced Concepts/pipelines-event-backtest.md`
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"""Pipeline API — cross-sectional momentum trading bot (Phase 1).
2+
3+
This example shows how to turn a :class:`Pipeline` screen into an actual
4+
trading bot that rebalances a portfolio every day into the top-N
5+
momentum names within a liquid universe.
6+
7+
What it does on each iteration (once per day):
8+
9+
1. The framework builds a long-form OHLCV panel across the configured
10+
symbols.
11+
2. ``MomentumScreener`` ranks every symbol by 30-day return within the
12+
top-3 most liquid names (by 30-day average dollar volume).
13+
3. ``CrossSectionalMomentumBot.run_strategy`` reads the resulting
14+
``polars.DataFrame`` from ``data["MomentumScreener"]``, picks the
15+
top-2 ranked symbols, and rebalances the portfolio:
16+
* closes any open position that is no longer in the target set,
17+
* opens an equal-weight market order for any new target.
18+
19+
Backtest the bot:
20+
21+
.. code-block:: bash
22+
23+
python examples/cross-sectional-pipelines/cross_sectional_momentum_bot.py
24+
25+
Run it live by removing the ``app.run_backtest(...)`` block and calling
26+
``app.run()`` instead. Bitvavo does not require API keys for market
27+
data, so the backtest works out of the box.
28+
29+
See docs:
30+
- ``docs/Advanced Concepts/pipelines.md``
31+
- ``docs/Advanced Concepts/pipelines-event-backtest.md``
32+
"""
33+
from __future__ import annotations
34+
35+
import logging.config
36+
from datetime import datetime, timedelta
37+
from typing import Any, Dict
38+
39+
from dotenv import load_dotenv
40+
41+
from investing_algorithm_framework import (
42+
AverageDollarVolume,
43+
BacktestDateRange,
44+
Context,
45+
DataSource,
46+
DEFAULT_LOGGING_CONFIG,
47+
OrderSide,
48+
OrderType,
49+
Pipeline,
50+
Returns,
51+
TimeUnit,
52+
TradingStrategy,
53+
create_app,
54+
)
55+
56+
logging.config.dictConfig(DEFAULT_LOGGING_CONFIG)
57+
load_dotenv()
58+
59+
60+
# Universe
61+
# A small crypto basket. All symbols quote in EUR so they share the
62+
# same trading symbol (the portfolio currency below).
63+
SYMBOLS = [
64+
"BTC/EUR",
65+
"ETH/EUR",
66+
"SOL/EUR",
67+
"ADA/EUR",
68+
"XRP/EUR",
69+
"DOT/EUR",
70+
"LINK/EUR",
71+
]
72+
73+
#: Number of symbols held simultaneously.
74+
TOP_N = 2
75+
76+
#: Exchange and quote currency for the bot.
77+
MARKET = "bitvavo"
78+
TRADING_SYMBOL = "EUR"
79+
80+
81+
# Pipeline: declarative cross-sectional screen
82+
class MomentumScreener(Pipeline):
83+
"""Rank the most liquid 3 names by 30-day return."""
84+
85+
# Per-symbol factors. Each becomes an output column.
86+
dollar_volume = AverageDollarVolume(window=30)
87+
momentum = Returns(window=30)
88+
89+
# The master mask: only the 3 most liquid names enter ranking.
90+
universe = dollar_volume.top(3)
91+
92+
# Cross-sectional rank within the universe (highest momentum gets
93+
# the highest rank).
94+
alpha = momentum.rank(mask=universe)
95+
96+
97+
# Strategy: rebalance into the top-N pipeline picks
98+
class CrossSectionalMomentumBot(TradingStrategy):
99+
algorithm_id = "pipeline-momentum-bot"
100+
time_unit = TimeUnit.DAY
101+
interval = 1
102+
market = MARKET
103+
trading_symbol = TRADING_SYMBOL
104+
symbols = SYMBOLS
105+
106+
# OHLCV data sources, one per symbol. ``warmup_window`` covers the
107+
# longest factor lookback (30) plus a buffer so the pipeline starts
108+
# producing values from bar 1 of the backtest. Tickers are added so
109+
# the bot can read live bid/ask in production — in backtest mode the
110+
# framework will fall back to the latest OHLCV close.
111+
data_sources = [
112+
DataSource(
113+
data_type="OHLCV",
114+
market=MARKET,
115+
symbol=symbol,
116+
warmup_window=60,
117+
time_frame="1d",
118+
identifier=f"{symbol}-ohlcv",
119+
)
120+
for symbol in SYMBOLS
121+
]
122+
123+
# Opt-in to the Pipeline API. The framework will compute this every
124+
# iteration and place the result under ``data["MomentumScreener"]``.
125+
pipelines = [MomentumScreener]
126+
127+
# Strategy entry point
128+
def run_strategy(self, context: Context, data: Dict[str, Any]) -> None:
129+
screen = data["MomentumScreener"]
130+
131+
# Skip iterations where the universe / warmup is not satisfied.
132+
if screen.is_empty():
133+
return
134+
135+
# Rank is ascending — highest rank = highest momentum.
136+
targets_df = screen.sort("alpha", descending=True).head(TOP_N)
137+
targets = {row["symbol"] for row in targets_df.iter_rows(named=True)}
138+
139+
def _last_close(symbol: str) -> float:
140+
ohlcv = data[f"{symbol}-ohlcv"]
141+
try:
142+
return float(ohlcv["Close"][-1])
143+
except (KeyError, TypeError):
144+
return float(ohlcv["close"].iloc[-1])
145+
146+
def _base(symbol: str) -> str:
147+
# ``target_symbol`` is just the base asset (e.g. "BTC").
148+
return symbol.split("/")[0]
149+
150+
# 1. Close positions no longer in the target set
151+
for symbol in SYMBOLS:
152+
if symbol in targets:
153+
continue
154+
base = _base(symbol)
155+
if not context.has_position(base, market=self.market):
156+
continue
157+
position = context.get_position(base, market=self.market)
158+
context.create_order(
159+
target_symbol=base,
160+
order_side=OrderSide.SELL,
161+
order_type=OrderType.LIMIT,
162+
price=_last_close(symbol),
163+
amount=position.get_amount(),
164+
)
165+
166+
# 2. Open new equal-weight positions for new targets
167+
unallocated = context.get_unallocated()
168+
new_targets = [
169+
sym for sym in targets
170+
if not context.has_position(_base(sym), market=self.market)
171+
]
172+
if not new_targets:
173+
return
174+
175+
per_target_budget = unallocated / len(new_targets)
176+
for symbol in new_targets:
177+
price = _last_close(symbol)
178+
if price <= 0:
179+
continue
180+
# 0.5% safety buffer so floating-point rounding does not
181+
# push the order total above the available unallocated cash.
182+
amount = (per_target_budget * 0.995) / price
183+
context.create_order(
184+
target_symbol=_base(symbol),
185+
order_side=OrderSide.BUY,
186+
order_type=OrderType.LIMIT,
187+
price=price,
188+
amount=amount,
189+
)
190+
191+
192+
# App wiring
193+
app = create_app()
194+
app.add_strategy(CrossSectionalMomentumBot)
195+
app.add_market(market=MARKET, trading_symbol=TRADING_SYMBOL, initial_balance=1000)
196+
197+
198+
if __name__ == "__main__":
199+
# Backtest over the last full year. Replace with ``app.run()`` to go
200+
# live (requires bitvavo credentials in your .env file).
201+
end = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
202+
start = end - timedelta(days=365)
203+
backtest_date_range = BacktestDateRange(start_date=start, end_date=end)
204+
backtest = app.run_backtest(
205+
backtest_date_range=backtest_date_range,
206+
)
207+
# Inspect the result interactively with the BacktestReport dashboard:
208+
# from investing_algorithm_framework import BacktestReport
209+
# BacktestReport(backtest).show()
210+
print(backtest)

0 commit comments

Comments
 (0)