feat: near-close scan schedule and distinct-day gate reset

Move the only qualifying R:R scan to 15:30 ET with chained Telegram alerts,
put outcome eval after a final-bar OHLCV fetch, enforce NY trading-day
requalify semantics, stamp paper trades fill_mode=near_close, and migrate
stored schedule_* keys to America/New_York.
This commit is contained in:
2026-07-18 17:55:39 +02:00
parent 5a61b164f6
commit 736451e26f
14 changed files with 428 additions and 83 deletions
@@ -0,0 +1,73 @@
"""near-close schedule cutover + paper trade fill_mode era tag
Revision ID: 023
Revises: 022
Create Date: 2026-07-18 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "023"
down_revision: Union[str, None] = "022"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
# Deliberate schedule rewrite (not a soft defaults refresh). Old stored values
# are logged then replaced so prod does not keep scanning at 07:00 Berlin.
_SCHEDULE_REWRITE: dict[str, str] = {
"schedule_timezone": "America/New_York",
"schedule_daily_pipeline_cron": "0 2 * * *",
"schedule_near_close_pipeline_cron": "30 15 * * 1-5",
"schedule_after_close_pipeline_cron": "45 16 * * 1-5",
"schedule_intraday_pipeline_cron": "0 10-15 * * 1-5",
"schedule_fundamentals_cron": "0 1 * * 1",
}
def upgrade() -> None:
op.add_column(
"paper_trades",
sa.Column("fill_mode", sa.String(length=20), nullable=True),
)
conn = op.get_bind()
settings = sa.table(
"system_settings",
sa.column("id", sa.Integer),
sa.column("key", sa.String),
sa.column("value", sa.Text),
sa.column("updated_at", sa.DateTime(timezone=True)),
)
now = sa.func.now()
for key, new_value in _SCHEDULE_REWRITE.items():
row = conn.execute(
sa.select(settings.c.value).where(settings.c.key == key)
).fetchone()
old_value = row[0] if row is not None else None
# Always log so ops can recover the pre-cutover schedule from migration output.
print(
f"schedule_cutover {key}: {old_value!r} -> {new_value!r}",
flush=True,
)
if row is None:
conn.execute(
sa.insert(settings).values(
key=key, value=new_value, updated_at=now
)
)
else:
conn.execute(
sa.update(settings)
.where(settings.c.key == key)
.values(value=new_value, updated_at=now)
)
def downgrade() -> None:
op.drop_column("paper_trades", "fill_mode")
# Do not restore old crons — unknown prior values; leave stored schedule as-is.