chart/app/bars/aggregator.py
Chris Amow 8c2ef80966 Remove the 4h timeframe
It was never in the enabled timeframes, so it held zero bars and produced no
levels, but it still carried weight 8 in the scoring table and forced
bucket_start to special-case a wall-clock ET anchor whose entire purpose was
surviving DST transitions. That was the most intricate logic in session.py,
maintained for a timeframe nobody used.

Daily is now the only session-anchored bucket, which is a much easier rule to
state and to keep correct. The DST parametrised tests go with it; the Sunday
open and daily boundary cases remain.

Manual-line tests move to 1h, so the weight assertions drop from 8 to 4.

The plan document keeps its 4h examples — rewriting a dozen illustrative
sentences would churn more than it clarifies — but the timeframe-roles section
now records the removal so nothing reads as a spec to build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 00:43:47 -05:00

57 lines
2 KiB
Python

from dataclasses import replace
from app.bars.models import Bar, Timeframe
from app.bars.session import bucket_start
class Aggregator:
def __init__(self, timeframes: list[Timeframe] | None = None):
self.timeframes = timeframes or list(Timeframe)
self.forming: dict[Timeframe, Bar] = {}
@staticmethod
def _can_aggregate(source: Timeframe, target: Timeframe) -> bool:
if source is Timeframe.M1:
return True
if source is Timeframe.H1:
return target in (Timeframe.H1, Timeframe.D1)
return source is target
def update(self, incoming: Bar) -> list[Bar]:
emitted: list[Bar] = []
for tf in self.timeframes:
if not self._can_aggregate(incoming.tf, tf):
continue
if tf is incoming.tf:
emitted.append(replace(incoming))
continue
start = bucket_start(incoming.t, tf)
current = self.forming.get(tf)
if current is not None and start < current.t:
continue
if current is None or start > current.t:
if current is not None:
emitted.append(replace(current, closed=True))
current = Bar(
tf=tf,
t=start,
o=incoming.o,
h=incoming.h,
l=incoming.l,
c=incoming.c,
v=incoming.v,
closed=False,
symbol=incoming.symbol,
source=incoming.source,
)
self.forming[tf] = current
else:
current.h = max(current.h, incoming.h)
current.l = min(current.l, incoming.l)
current.c = incoming.c
current.v += incoming.v
current.symbol = incoming.symbol
current.source = incoming.source
emitted.append(replace(current))
return emitted