Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 30 additions & 18 deletions backend_api_python/app/services/strategy_v2/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1910,27 +1910,39 @@ def _schedule_due(
) -> bool:
current = pd.Timestamp(current)
previous = pd.Timestamp(previous) if previous is not None else None
scheduled_date = current.normalize()
if schedule.frequency == "weekly":
target_weekday = max(1, min(7, int(schedule.weekday or 1))) - 1
scheduled_date = current.normalize() + pd.Timedelta(days=target_weekday - current.weekday())
elif schedule.frequency == "monthly":
target_day = max(1, int(schedule.monthday or 1))
last_day = calendar.monthrange(current.year, current.month)[1]
scheduled_date = pd.Timestamp(
year=current.year,
month=current.month,
day=min(target_day, last_day),
tz=current.tz,
)
elif schedule.frequency != "daily":
if schedule.frequency not in {"daily", "weekly", "monthly"}:
return False

scheduled_at = scheduled_date
if _is_intraday_frequency(bar_frequency) and schedule.time:
scheduled_at += _parse_schedule_time(schedule.time)
def occurrence(anchor: pd.Timestamp) -> pd.Timestamp:
scheduled_date = anchor.normalize()
if schedule.frequency == "weekly":
target_weekday = max(1, min(7, int(schedule.weekday or 1))) - 1
scheduled_date += pd.Timedelta(days=target_weekday - anchor.weekday())
elif schedule.frequency == "monthly":
target_day = max(1, int(schedule.monthday or 1))
last_day = calendar.monthrange(anchor.year, anchor.month)[1]
scheduled_date = pd.Timestamp(
year=anchor.year,
month=anchor.month,
day=min(target_day, last_day),
tz=anchor.tz,
)
if _is_intraday_frequency(bar_frequency) and schedule.time:
scheduled_date += _parse_schedule_time(schedule.time)
return scheduled_date

scheduled_at = occurrence(current)
if current < scheduled_at:
return False
if previous is None or schedule.frequency == "daily":
return False
# The target day of the previous period may have had no bar
# (weekend, holiday). Catch it up on the first bar after it, as
# already happens when the gap stays inside one period.
if schedule.frequency == "weekly":
scheduled_at = occurrence(current - pd.DateOffset(days=7))
else:
scheduled_at = occurrence(current.replace(day=1) - pd.DateOffset(days=1))
return previous < scheduled_at <= current
if previous is None:
return True
if schedule.frequency == "daily" and not _is_intraday_frequency(bar_frequency):
Expand Down
45 changes: 45 additions & 0 deletions backend_api_python/tests/test_strategy_v2_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,51 @@ def test_scheduler_honors_weekday_monthday_and_intraday_time():
)


def test_scheduler_catches_up_target_day_missed_at_period_boundary():
good_friday_weekly = ScheduleSpec("weekly", "rebalance", weekday=5)
month_end = ScheduleSpec("monthly", "rebalance", monthday=31)
weekend_weekly = ScheduleSpec("weekly", "rebalance", weekday=6)

# 2026-04-03 is Good Friday (no US session): the week must still run once.
assert StrategyV2BacktestRunner._schedule_due(
good_friday_weekly, pd.Timestamp("2026-04-06"), pd.Timestamp("2026-04-02"), "1d"
)
assert not StrategyV2BacktestRunner._schedule_due(
good_friday_weekly, pd.Timestamp("2026-04-07"), pd.Timestamp("2026-04-06"), "1d"
)
# 2026-01-31 is a Saturday: the January month-end run happens on 2026-02-02.
assert StrategyV2BacktestRunner._schedule_due(
month_end, pd.Timestamp("2026-02-02"), pd.Timestamp("2026-01-30"), "1d"
)
assert not StrategyV2BacktestRunner._schedule_due(
month_end, pd.Timestamp("2026-02-03"), pd.Timestamp("2026-02-02"), "1d"
)
# A Saturday target on a weekday-only market runs on the following Monday.
assert StrategyV2BacktestRunner._schedule_due(
weekend_weekly, pd.Timestamp("2026-01-12"), pd.Timestamp("2026-01-09"), "1d"
)
assert not StrategyV2BacktestRunner._schedule_due(
weekend_weekly, pd.Timestamp("2026-01-13"), pd.Timestamp("2026-01-12"), "1d"
)
# Intraday: a Monday bar before the scheduled time does not re-run last week.
intraday = ScheduleSpec("weekly", "rebalance", weekday=1, time="09:35")
assert not StrategyV2BacktestRunner._schedule_due(
intraday, pd.Timestamp("2026-01-12 09:30"), pd.Timestamp("2026-01-09 16:00"), "5m"
)
assert not StrategyV2BacktestRunner._schedule_due(
good_friday_weekly, pd.Timestamp("2026-04-06"), None, "1d"
)
# Live clocks are zone-aware: stepping back a week must not cross into
# the week before when the gap spans a DST change (2026-03-08 in New York).
live = ScheduleSpec("weekly", "rebalance", weekday=6, time="09:35")
assert StrategyV2BacktestRunner._schedule_due(
live,
pd.Timestamp("2026-03-09 00:30", tz="America/New_York"),
pd.Timestamp("2026-03-06 16:00", tz="America/New_York"),
"1m",
)


def test_rejected_and_deferred_orders_are_visible_in_audit_ledger():
frame = _frame([100, 101, 102])
frame["is_suspended"] = [False, True, False]
Expand Down