Skip to content

Commit 550f7d8

Browse files
Pigbibicodex
andcommitted
fix: tighten review date and maturity boundaries
Co-Authored-By: Codex <noreply@openai.com>
1 parent 6694df4 commit 550f7d8

2 files changed

Lines changed: 52 additions & 7 deletions

File tree

src/quant_advisor_research/recommendation_review.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ def first_bar_on_or_after(bars: list[PriceBar], target: dt.date) -> PriceBar | N
5555
return None
5656

5757

58+
def start_bar_for_report(bars: list[PriceBar], target: dt.date) -> PriceBar | None:
59+
ordered = sorted(bars, key=lambda item: item.date)
60+
exact = next((bar for bar in ordered if bar.date == target), None)
61+
if exact:
62+
return exact
63+
previous = last_bar_on_or_before(ordered, target)
64+
if previous and (target - previous.date).days <= MAX_START_BAR_DELAY_DAYS:
65+
return previous
66+
return first_bar_on_or_after(ordered, target)
67+
68+
5869
def last_bar_on_or_before(bars: list[PriceBar], target: dt.date) -> PriceBar | None:
5970
candidates = [bar for bar in bars if bar.date <= target]
6071
return max(candidates, key=lambda item: item.date) if candidates else None
@@ -72,16 +83,16 @@ def outcome_label(
7283
elapsed_days: int,
7384
has_price_data: bool,
7485
horizon: str,
75-
trading_observations: int,
86+
trading_intervals: int,
7687
) -> str:
7788
if elapsed_days <= 0:
7889
return "pending"
7990
if not has_price_data:
8091
return "insufficient_price_data"
92+
if trading_intervals < MIN_MATURITY_TRADING_DAYS.get(horizon, MIN_MATURITY_TRADING_DAYS["medium"]):
93+
return "in_progress"
8194
if relative_return is None:
8295
return "insufficient_price_data"
83-
if trading_observations < MIN_MATURITY_TRADING_DAYS.get(horizon, MIN_MATURITY_TRADING_DAYS["medium"]):
84-
return "in_progress"
8596
if relative_return >= 0.02:
8697
return "outperforming"
8798
if relative_return <= -0.02:
@@ -123,9 +134,9 @@ def build_review_item(
123134
data_source: str,
124135
) -> dict[str, Any]:
125136
symbol = str(pick.get("symbol", "")).upper()
126-
start_bar = first_bar_on_or_after(symbol_bars, report_as_of)
137+
start_bar = start_bar_for_report(symbol_bars, report_as_of)
127138
end_bar = last_bar_on_or_before(symbol_bars, review_as_of)
128-
benchmark_start = first_bar_on_or_after(benchmark_bars, report_as_of)
139+
benchmark_start = start_bar_for_report(benchmark_bars, report_as_of)
129140
benchmark_end = last_bar_on_or_before(benchmark_bars, review_as_of)
130141
has_price_data = bool(start_bar and end_bar and start_bar.date <= end_bar.date)
131142

@@ -147,11 +158,12 @@ def build_review_item(
147158
elapsed_days = (review_as_of - report_as_of).days
148159
horizon = str(pick.get("primary_horizon", ""))
149160
maturity_days = MIN_MATURITY_TRADING_DAYS.get(horizon, MIN_MATURITY_TRADING_DAYS["medium"])
161+
trading_intervals = max(trading_observations - 1, 0)
150162
if elapsed_days <= 0:
151163
maturity_status = "pending"
152164
elif not has_price_data:
153165
maturity_status = "insufficient_price_data"
154-
elif trading_observations < maturity_days:
166+
elif trading_intervals < maturity_days:
155167
maturity_status = "in_progress"
156168
else:
157169
maturity_status = "matured"
@@ -166,6 +178,7 @@ def build_review_item(
166178
"end_price_date": end_date,
167179
"elapsed_calendar_days": elapsed_days,
168180
"trading_observations": trading_observations,
181+
"trading_intervals": trading_intervals,
169182
"maturity_required_trading_days": maturity_days,
170183
"maturity_status": maturity_status,
171184
"absolute_return": absolute_return,
@@ -176,7 +189,7 @@ def build_review_item(
176189
elapsed_days=elapsed_days,
177190
has_price_data=has_price_data,
178191
horizon=horizon,
179-
trading_observations=trading_observations,
192+
trading_intervals=trading_intervals,
180193
),
181194
"market_data_source": data_source if has_price_data else "",
182195
"combined_score": pick.get("combined_score"),

tests/test_recommendation_review.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,38 @@ def test_review_requires_price_coverage_on_or_before_report_date(tmp_path: Path)
149149
assert item["outcome"] == "insufficient_price_data"
150150

151151

152+
def test_weekend_report_uses_previous_trading_close(tmp_path: Path) -> None:
153+
cache_dir = tmp_path / "market-cache"
154+
trading_dates = [dt.date(2026, 1, 9), dt.date(2026, 1, 12), dt.date(2026, 1, 13)]
155+
write_cached_bars("MU", [PriceBar(date=date, close=100 + index, volume=1000) for index, date in enumerate(trading_dates)], cache_dir=cache_dir)
156+
write_cached_bars("SPY", [PriceBar(date=date, close=100, volume=1000) for date in trading_dates], cache_dir=cache_dir)
157+
report_path = tmp_path / "advisory_report_2026-01-10.json"
158+
write_report(report_path, as_of="2026-01-10", horizon="short")
159+
160+
review = build_recommendation_review(
161+
report_paths=[report_path], as_of=dt.date(2026, 1, 20), benchmark="SPY",
162+
cache_dir=cache_dir, cache_max_age_days=14, use_network=False,
163+
)
164+
165+
assert review["review_items"][0]["start_price_date"] == "2026-01-09"
166+
167+
168+
def test_pre_maturity_item_stays_in_progress_when_benchmark_is_unavailable(tmp_path: Path) -> None:
169+
cache_dir = tmp_path / "market-cache"
170+
write_cached_bars("MU", make_bars(dt.date(2026, 1, 5), [100] * 6), cache_dir=cache_dir)
171+
report_path = tmp_path / "advisory_report_2026-01-05.json"
172+
write_report(report_path, horizon="short")
173+
174+
review = build_recommendation_review(
175+
report_paths=[report_path], as_of=dt.date(2026, 1, 10), benchmark="SPY",
176+
cache_dir=cache_dir, cache_max_age_days=14, use_network=False,
177+
)
178+
179+
item = review["review_items"][0]
180+
assert item["maturity_status"] == "in_progress"
181+
assert item["outcome"] == "in_progress"
182+
183+
152184
def test_recommendation_review_marks_same_day_report_as_pending(tmp_path: Path) -> None:
153185
report_path = tmp_path / "advisory_report_2026-01-05.json"
154186
write_report(report_path)

0 commit comments

Comments
 (0)