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>
57 lines
2 KiB
Python
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
|