@@ -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+
5869def 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" ),
0 commit comments