Skip to content

Commit eb4e57b

Browse files
committed
Add another end-to-end test over a single market movement (1 decision)
Fix the implementation of BacktestResults.nav, as well as change .holdings (which is slightly ambiguous) into quantities_held and values_held
1 parent 166dfb4 commit eb4e57b

File tree

5 files changed

+75
-5
lines changed

5 files changed

+75
-5
lines changed

src/backtest_lib/backtest/results.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def from_weights_market_initial_capital(
259259
return results
260260

261261
@cached_property
262-
def holdings(self) -> PastView[float, IndexT]:
262+
def quantities_held(self) -> PastView[float, IndexT]:
263263
weights = self.weights.by_security.to_dataframe(lazy=True)
264264
prices = self.market.prices.close.by_security.to_dataframe(lazy=True)
265265

@@ -273,10 +273,35 @@ def holdings(self) -> PastView[float, IndexT]:
273273
)
274274
if dtype.is_numeric()
275275
]
276+
nav_series = pl.Series(self.nav)
276277

277-
result = joined.select(
278+
qtys = joined.select(
278279
"date",
279-
*[(pl.col(c) * pl.col(f"{c}_p")).alias(c) for c in numeric_cols],
280+
*[
281+
(pl.col(c) * nav_series / pl.col(f"{c}_p")).alias(c)
282+
for c in numeric_cols
283+
],
280284
)
281285

282-
return self._backend.from_dataframe(result.collect())
286+
return self._backend.from_dataframe(qtys.collect())
287+
288+
@cached_property
289+
def values_held(self) -> PastView[float, IndexT]:
290+
weights = self.weights.by_security.to_dataframe(lazy=True)
291+
292+
weights_schema = weights.collect_schema()
293+
numeric_cols = [
294+
name
295+
for name, dtype in zip(
296+
weights_schema.names(), weights_schema.dtypes(), strict=True
297+
)
298+
if dtype.is_numeric()
299+
]
300+
nav_series = pl.Series(self.nav)
301+
302+
values = weights.select(
303+
"date",
304+
*[(pl.col(c) * nav_series).alias(c) for c in numeric_cols],
305+
)
306+
307+
return self._backend.from_dataframe(values.collect())

src/backtest_lib/market/polars_impl/_past_view.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import numpy as np
1818
import polars as pl
19+
from polars.exceptions import InvalidOperationError
1920

2021
from backtest_lib.market import ByPeriod, BySecurity, Closed, PastView
2122
from backtest_lib.market.plotting import (
@@ -525,7 +526,16 @@ def from_dataframe(df: pl.DataFrame | pd.DataFrame) -> Self:
525526
) from e
526527

527528
if dates.dtype not in (pl.Date, pl.Datetime):
528-
dates = dates.cast(pl.Datetime("us"))
529+
if dates.dtype == pl.String:
530+
dates = dates.str.to_datetime(time_unit="us")
531+
else:
532+
try:
533+
dates = dates.cast(pl.Datetime("us"))
534+
except InvalidOperationError as e:
535+
raise ValueError(
536+
"Cannot convert 'date' column with polars type "
537+
f"{dates.dtype} to polars.Datetime)"
538+
) from e
529539

530540
period_names = dates.dt.to_string()
531541
non_date_cols = [x for x in df.columns if x != "date"]

tests/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,10 @@ def single_security_market(test_data_dir) -> MarketView:
1616
data = read_csv(test_data_dir / "single_security.csv")
1717
market = MarketView(data)
1818
return market
19+
20+
21+
@pytest.fixture(scope="session")
22+
def simple_market(test_data_dir) -> MarketView:
23+
data = read_csv(test_data_dir / "simple_market.csv")
24+
market = MarketView(data)
25+
return market

tests/data/simple_market.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
date,sec1,sec2
2+
2025-01-01,100.0,10.0
3+
2025-01-02,200.0,10.0

tests/e2e/short_schedule_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import backtest_lib as btl
2+
from backtest_lib import target_weights
3+
from backtest_lib.engine.decision import (
4+
Decision,
5+
)
6+
from backtest_lib.portfolio import uniform_portfolio
7+
8+
9+
def test_over_single_market_movement(simple_market):
10+
market = simple_market
11+
initial_capital = 1_000_000
12+
initial_portfolio = uniform_portfolio(market.securities, value=initial_capital)
13+
14+
def strategy(*args, **kwargs) -> Decision:
15+
return target_weights({"sec1": 0.5, "sec2": 0.5})
16+
17+
backtest = btl.Backtest(
18+
strategy=strategy, market_view=market, initial_portfolio=initial_portfolio
19+
)
20+
results = backtest.run()
21+
22+
assert list(results.quantities_held.by_security["sec1"]) == [5000, 3750]
23+
assert list(results.quantities_held.by_security["sec2"]) == [50000, 75000]
24+
assert results.nav == [initial_capital, initial_capital * 1.5]
25+
assert results.total_return == 0.5

0 commit comments

Comments
 (0)