feat(b3b): daily_returns_for_weights (fixed-weight holding-period book returns)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
|
||||
from fxhnt.domain.strategies.equity_factor import book_return
|
||||
|
||||
_SECONDS_PER_DAY = 86_400
|
||||
|
||||
|
||||
@@ -43,3 +45,23 @@ def month_end_rebalance_dates(dates: list[str]) -> list[str]:
|
||||
y, m, _ = d.split("-")
|
||||
last_by_month[(int(y), int(m))] = d
|
||||
return [last_by_month[k] for k in sorted(last_by_month)]
|
||||
|
||||
|
||||
def daily_returns_for_weights(
|
||||
weights: dict[str, float],
|
||||
closes: dict[str, dict[int, float]],
|
||||
days: list[int],
|
||||
borrow_daily: float,
|
||||
) -> list[float]:
|
||||
"""Daily book returns of fixed `weights` over consecutive `days` (epoch-days,
|
||||
ascending). For each adjacent (prev, cur) pair, both prices must exist for a
|
||||
symbol to contribute; a step with no usable price pair is dropped (not 0.0)."""
|
||||
out: list[float] = []
|
||||
for prev, cur in zip(days, days[1:]):
|
||||
prev_p = {s: closes[s][prev] for s in weights if s in closes and prev in closes[s] and cur in closes[s]}
|
||||
cur_p = {s: closes[s][cur] for s in prev_p}
|
||||
if not prev_p:
|
||||
continue
|
||||
w = {s: weights[s] for s in prev_p}
|
||||
out.append(book_return(w, prev_p, cur_p, borrow_daily=borrow_daily))
|
||||
return out
|
||||
|
||||
@@ -40,3 +40,32 @@ def test_month_end_rebalance_dates_one_per_month():
|
||||
def test_epoch_day_roundtrip():
|
||||
assert _epoch_day("1970-01-01") == 0
|
||||
assert _epoch_day("1970-01-02") == 1
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from fxhnt.domain.equity_backtest import daily_returns_for_weights
|
||||
|
||||
|
||||
def test_daily_returns_long_only_matches_weighted_price_change():
|
||||
weights = {"A": 0.5, "B": 0.5}
|
||||
closes = {
|
||||
"A": {0: 100.0, 1: 110.0},
|
||||
"B": {0: 50.0, 1: 50.0},
|
||||
}
|
||||
rets = daily_returns_for_weights(weights, closes, [0, 1], borrow_daily=0.0)
|
||||
assert rets == pytest.approx([0.05])
|
||||
|
||||
|
||||
def test_daily_returns_applies_borrow_on_short_notional():
|
||||
weights = {"A": -1.0}
|
||||
closes = {"A": {0: 100.0, 1: 100.0}}
|
||||
rets = daily_returns_for_weights(weights, closes, [0, 1], borrow_daily=0.001)
|
||||
assert rets == [-0.001]
|
||||
|
||||
|
||||
def test_daily_returns_skips_day_with_missing_price():
|
||||
weights = {"A": 1.0}
|
||||
closes = {"A": {0: 100.0, 2: 121.0}} # day 1 missing
|
||||
rets = daily_returns_for_weights(weights, closes, [0, 1, 2], borrow_daily=0.0)
|
||||
assert rets == []
|
||||
|
||||
Reference in New Issue
Block a user